Repository: pydantic/pydantic

Issue:
When using `min_items` and `max_items` pydantic cannot accept `None`
# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.4
            pydantic compiled: False
                 install path: /Users/spike/.pyenv/versions/3.8.2/lib/python3.8/site-packages/pydantic
               python version: 3.8.2 (default, Feb 26 2020, 16:45:04)  [Clang 11.0.0 (clang-1100.0.33.17)]
                     platform: macOS-10.15.3-x86_64-i386-64bit
     optional deps. installed: []

```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->

```py
>>> class Foo(BaseModel):
...     bar: List = Field(None)
...     baz: List = Field(None, max_items=10)
...
>>> Foo()
Foo(bar=None, baz=None)
>>> Foo(bar=None)
Foo(bar=None, baz=None)
>>> Foo(baz=None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/spike/.pyenv/versions/3.8.2/lib/python3.8/site-packages/pydantic/main.py", line 283, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Foo
baz
  value is not a valid list (type=type_error.list)
```

It is not what I expected.

Also when using `min_items`

```py
>>> class Foo(BaseModel):
...     bar: List = Field(None, min_items=0)
...
>>> Foo()
Foo(bar=None)
>>> Foo(bar=None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/spike/.pyenv/versions/3.8.2/lib/python3.8/site-packages/pydantic/main.py", line 283, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Foo
bar
  value is not a valid list (type=type_error.list)
```

I think both `min_items` and `max_items` expect that an attribute must iterable. It is the same when using `Optional`.



Discussion:
agreed, thanks for reporting.

PR welcome, otherwise I'll try and fix when I get a chance.

Resolving patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -81,6 +81,7 @@
     from .dataclasses import DataclassType  # noqa: F401
     from .main import BaseModel, BaseConfig  # noqa: F401
     from .typing import CallableGenerator
+    from .fields import ModelField
 
     ModelOrDc = Type[Union['BaseModel', 'DataclassType']]
 
@@ -122,7 +123,6 @@ class ConstrainedList(list):  # type: ignore
 
     @classmethod
     def __get_validators__(cls) -> 'CallableGenerator':
-        yield list_validator
         yield cls.list_length_validator
 
     @classmethod
@@ -130,7 +130,11 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
         update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items)
 
     @classmethod
-    def list_length_validator(cls, v: 'List[T]') -> 'List[T]':
+    def list_length_validator(cls, v: 'Optional[List[T]]', field: 'ModelField') -> 'Optional[List[T]]':
+        if v is None and not field.required:
+            return None
+
+        v = list_validator(v)
         v_len = len(v)
 
         if cls.min_items is not None and v_len < cls.min_items:


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,3 +1,4 @@
+import sys
 from enum import Enum
 from typing import Any, Callable, ClassVar, List, Mapping, Type
 from uuid import UUID, uuid4
@@ -1025,3 +1026,21 @@ class FunctionModel(BaseModel):
 
     m = FunctionModel()
     assert m.uid is uuid4
+
+
+@pytest.mark.skipif(sys.version_info < (3, 7), reason='field constraints are set but not enforced with python 3.6')
+def test_none_min_max_items():
+    # None default
+    class Foo(BaseModel):
+        foo: List = Field(None)
+        bar: List = Field(None, min_items=0)
+        baz: List = Field(None, max_items=10)
+
+    f1 = Foo()
+    f2 = Foo(bar=None)
+    f3 = Foo(baz=None)
+    f4 = Foo(bar=None, baz=None)
+    for f in (f1, f2, f3, f4):
+        assert f.foo is None
+        assert f.bar is None
+        assert f.baz is None