Repository: pydantic/pydantic

Issue:
Incomplete type annotations in BaseConfig yield mypy errors when Config is defined from BaseConfig
### Checks

* [X] I added a descriptive title to this issue
* [X] I have searched (google, github) for similar issues and couldn't find anything
* [X] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

1. The `BaseConfig` is missing type annotations for a few fields. Inheriting from `BaseConfig` and setting values yields type errors from `mypy`. 

```python
from pydantic import BaseConfig, BaseModel, Extra


class Record(BaseModel):
    alpha: str
    beta: str

    class Config(BaseConfig):
        title = "Record"
        extra = Extra.ignore
        max_anystr_length = 1234

```

yields

```bash
mypy --version && mypy example.py
mypy 0.812
example.py:9: error: Incompatible types in assignment (expression has type "str", base class "BaseConfig" defined the type as "None")  [assignment]
example.py:11: error: Incompatible types in assignment (expression has type "int", base class "BaseConfig" defined the type as "None")  [assignment]
Found 2 errors in 1 file (checked 1 source file)
```

I believe the type annotations should be:

```python
from typing import Union, Optional

class BaseConfig:
    title: Optional[str] = None
    min_anystr_length: Optional[int] = None # can this just be int = 0 ?
    max_anystr_length: Optional[int] = None
    extra = Union[str, Extra] = Extra.ignore
```

Inheriting from `BaseConfig` is useful for autocomplete and avoids typos like the following.

```python
from pydantic import BaseModel

class Point(BaseModel):
    x: int
    y: int
    
    class Config:
        extras = "forbid"
```

2. The [Config docs](https://pydantic-docs.helpmanual.io/usage/model_config/) look to be inconsistent with the default values defined in `BaseConfig`.

- the default for `max_anystr_length` is listed as `2 ** 16` in the docs, however, `None` is defined in `BaseConfig` (i.e., there is no limit)
- Setting `max_anystr_length = 0` ignores the validation completely and has a different semantic meaning than any other positive integer. In other words, `max_anystr_length = 0` is the same as `max_anystr_length = None`.
- Similarly, the default for `min_anystr_length` is listed as `0` in the docs, but the listed default is `None` in `BaseConfig`. If these are semantically the same, then perhaps set `min_anystr_length:int = 0` as the default?
- In general, it might be useful to clarify the truthyness of these `0` vs `None` values 

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.8.1
            pydantic compiled: False
               python version: 3.9.2 | packaged by conda-forge | (default, Feb 21 2021, 05:02:20)  [Clang 11.0.1 ]
                     platform: macOS-11.2.3-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
```



Discussion:

Resolving patch:
diff --git a/pydantic/config.py b/pydantic/config.py
--- a/pydantic/config.py
+++ b/pydantic/config.py
@@ -38,21 +38,21 @@ class Extra(str, Enum):
 
 
 class BaseConfig:
-    title = None
-    anystr_lower = False
-    anystr_strip_whitespace = False
-    min_anystr_length = None
-    max_anystr_length = None
-    validate_all = False
-    extra = Extra.ignore
-    allow_mutation = True
-    frozen = False
-    allow_population_by_field_name = False
-    use_enum_values = False
+    title: Optional[str] = None
+    anystr_lower: bool = False
+    anystr_strip_whitespace: bool = False
+    min_anystr_length: int = 0
+    max_anystr_length: Optional[int] = None
+    validate_all: bool = False
+    extra: Extra = Extra.ignore
+    allow_mutation: bool = True
+    frozen: bool = False
+    allow_population_by_field_name: bool = False
+    use_enum_values: bool = False
     fields: Dict[str, Union[str, Dict[str, str]]] = {}
-    validate_assignment = False
+    validate_assignment: bool = False
     error_msg_templates: Dict[str, str] = {}
-    arbitrary_types_allowed = False
+    arbitrary_types_allowed: bool = False
     orm_mode: bool = False
     getter_dict: Type[GetterDict] = GetterDict
     alias_generator: Optional[Callable[[str], str]] = None
diff --git a/pydantic/validators.py b/pydantic/validators.py
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -193,7 +193,7 @@ def anystr_length_validator(v: 'StrBytes', config: 'BaseConfig') -> 'StrBytes':
     v_len = len(v)
 
     min_length = config.min_anystr_length
-    if min_length is not None and v_len < min_length:
+    if v_len < min_length:
         raise errors.AnyStrMinLengthError(limit_value=min_length)
 
     max_length = config.max_anystr_length
@@ -470,11 +470,11 @@ def literal_validator(v: Any) -> Any:
 def constr_length_validator(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes':
     v_len = len(v)
 
-    min_length = field.type_.min_length or config.min_anystr_length
-    if min_length is not None and v_len < min_length:
+    min_length = field.type_.min_length if field.type_.min_length is not None else config.min_anystr_length
+    if v_len < min_length:
         raise errors.AnyStrMinLengthError(limit_value=min_length)
 
-    max_length = field.type_.max_length or config.max_anystr_length
+    max_length = field.type_.max_length if field.type_.max_length is not None else config.max_anystr_length
     if max_length is not None and v_len > max_length:
         raise errors.AnyStrMaxLengthError(limit_value=max_length)
 


Test patch:
diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py
--- a/tests/mypy/modules/success.py
+++ b/tests/mypy/modules/success.py
@@ -12,8 +12,10 @@
 
 from pydantic import (
     UUID1,
+    BaseConfig,
     BaseModel,
     DirectoryPath,
+    Extra,
     FilePath,
     FutureDate,
     Json,
@@ -239,3 +241,9 @@ class Config:
 validated.my_file_path_str.absolute()
 validated.my_dir_path.absolute()
 validated.my_dir_path_str.absolute()
+
+
+class Config(BaseConfig):
+    title = 'Record'
+    extra = Extra.ignore
+    max_anystr_length = 1234
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -572,6 +572,24 @@ class Model(BaseModel):
     assert m.v == 'ABCD'
 
 
+def test_constrained_str_max_length_0():
+    class Model(BaseModel):
+        v: constr(max_length=0)
+
+    m = Model(v='')
+    assert m.v == ''
+    with pytest.raises(ValidationError) as exc_info:
+        Model(v='qwe')
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('v',),
+            'msg': 'ensure this value has at most 0 characters',
+            'type': 'value_error.any_str.max_length',
+            'ctx': {'limit_value': 0},
+        }
+    ]
+
+
 def test_module_import():
     class PyObjectModel(BaseModel):
         module: PyObject = 'os.path'