Repository: samuelcolvin/pydantic (python)

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']
```


Pull request:
fix: add missing BaseConfig type annotations and handle `max_length = 0`
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary

<!-- Please give a short summary of the changes. -->

## Related issue number
closes #2719
<!-- Are there any issues opened that will be resolved by merging this change? -->

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Patch:
diff --git a/changes/2719-PrettyWood.md b/changes/2719-PrettyWood.md
new file mode 100644
index 000000000..90a037c07
--- /dev/null
+++ b/changes/2719-PrettyWood.md
@@ -0,0 +1,1 @@
+add missing type annotations in `BaseConfig` and handle `max_length = 0`
diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md
index 411112df1..ccfb7c9ff 100644
--- a/docs/usage/model_config.md
+++ b/docs/usage/model_config.md
@@ -32,7 +32,7 @@ _(This script is complete, it should run "as is")_
 : the min length for str & byte types (default: `0`)
 
 **`max_anystr_length`**
-: the max length for str & byte types (default: `2 ** 16`)
+: the max length for str & byte types (default: `None`)
 
 **`validate_all`**
 : whether to validate field defaults (default: `False`)
diff --git a/pydantic/config.py b/pydantic/config.py
index acd20da14..5cc7e3a74 100644
--- 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
index 82061110e..72baa0308 100644
--- 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 make_literal_validator(type_: Any) -> Callable[[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
index 55c687944..7aed98358 100644
--- a/tests/mypy/modules/success.py
+++ b/tests/mypy/modules/success.py
@@ -12,8 +12,10 @@ from uuid import UUID
 
 from pydantic import (
     UUID1,
+    BaseConfig,
     BaseModel,
     DirectoryPath,
+    Extra,
     FilePath,
     FutureDate,
     Json,
@@ -239,3 +241,9 @@ validated.my_file_path.absolute()
 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
index d40f8859a..833108476 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -572,6 +572,24 @@ def test_constrained_str_lower_disabled():
     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'