Repository: ipython/traitlets (python)

Issue:
Tuple cannot have default_value=None
When creating an `HasTraits` class with a tuple value that can be none, we get an error:

```python
from traitlets import HasTraits, Tuple

class Foo(HasTraits):
    value = Tuple(allow_none=True, default_value=None)
```

Shows:
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-54c571552fdc> in <module>
----> 1 class Foo(HasTraits):
      2     value = Tuple(allow_none=True, default_value=None)
      3 

<ipython-input-2-54c571552fdc> in Foo()
      1 class Foo(HasTraits):
----> 2     value = Tuple(allow_none=True, default_value=None)
      3 

~/lib/python3.8/site-packages/traitlets/traitlets.py in __init__(self, *traits, **kwargs)
   2414             args = (default_value,)
   2415         else:
-> 2416             raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value))
   2417 
   2418         self._traits = []

TypeError: default value of Tuple was None
```

Pull request description:
handle default_value=None in Container traits
closes #626

Patch:
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 5511e81..be50c2b 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -9,6 +9,7 @@ Traitlets 5.0
 
 - Support deprecated literals for sets, tuples on the command-line: ``nbconvert --TagRemovePreprocessor.remove_cell_tags='{"tag"}'``
 - Fix ``from_string_list`` for Tuples in general
+- Fix support for ``List(default_value=None, allow_none=True)`` and other Container traits
 
 5.0.4
 *****
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index b1894a4..2cd1453 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -1695,7 +1695,7 @@ class ClassBasedTraitType(TraitType):
 class Type(ClassBasedTraitType):
     """A trait whose value must be a subclass of a specified class."""
 
-    def __init__ (self, default_value=Undefined, klass=None, **kwargs):
+    def __init__(self, default_value=Undefined, klass=None, **kwargs):
         """Construct a Type trait
 
         A Type trait specifies that its values must be subclasses of
@@ -2411,10 +2411,9 @@ class Container(Instance):
     _cast_types = ()
     _valid_defaults = SequenceTypes
     _trait = None
-
     _literal_from_string_pairs = ("[]", "()")
 
-    def __init__(self, trait=None, default_value=None, **kwargs):
+    def __init__(self, trait=None, default_value=Undefined, **kwargs):
         """Create a container trait type from a list, set, or tuple.
 
         The default value is created by doing ``List(default_value)``,
@@ -2443,12 +2442,16 @@ class Container(Instance):
 
         """
         # allow List([values]):
-        if default_value is None and not is_trait(trait):
+        if trait is not None and default_value is Undefined and not is_trait(trait):
             default_value = trait
             trait = None
 
-        if default_value is None:
+        if default_value is Undefined:
             args = ()
+        elif default_value is None:
+            # default_value back on kwargs for super() to handle
+            args = ()
+            kwargs["default_value"] = None
         elif isinstance(default_value, self._valid_defaults):
             args = (default_value,)
         else:
@@ -2562,7 +2565,14 @@ class List(Container):
     klass = list
     _cast_types = (tuple,)
 
-    def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, **kwargs):
+    def __init__(
+        self,
+        trait=None,
+        default_value=Undefined,
+        minlen=0,
+        maxlen=sys.maxsize,
+        **kwargs,
+    ):
         """Create a List trait type from a list, set, or tuple.
 
         The default value is created by doing ``list(default_value)``,
@@ -2615,8 +2625,14 @@ class Set(List):
     _literal_from_string_pairs = ("[]", "()", "{}")
 
     # Redefine __init__ just to make the docstring more accurate.
-    def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize,
-                 **kwargs):
+    def __init__(
+        self,
+        trait=None,
+        default_value=Undefined,
+        minlen=0,
+        maxlen=sys.maxsize,
+        **kwargs,
+    ):
         """Create a Set trait type from a list, set, or tuple.
 
         The default value is created by doing ``set(default_value)``,
@@ -2694,6 +2710,10 @@ class Tuple(Container):
 
         if default_value is Undefined:
             args = ()
+        elif default_value is None:
+            # default_value back on kwargs for super() to handle
+            args = ()
+            kwargs["default_value"] = None
         elif isinstance(default_value, self._valid_defaults):
             args = (default_value,)
         else:


Test patch:
diff --git a/traitlets/tests/test_traitlets.py b/traitlets/tests/test_traitlets.py
index a5cdb2a..0680ab8 100644
--- a/traitlets/tests/test_traitlets.py
+++ b/traitlets/tests/test_traitlets.py
@@ -9,13 +9,11 @@
 
 import pickle
 import re
-import sys
-from ._warnings import expected_warnings
-
 from unittest import TestCase
+
 import pytest
-from pytest import mark
 
+from ._warnings import expected_warnings
 from traitlets import (
     HasTraits,
     MetaHasTraits,
@@ -40,6 +38,7 @@ from traitlets import (
     Callable,
     All,
     Undefined,
+    Set,
     Type,
     This,
     Instance,
@@ -1517,10 +1516,12 @@ class TestTCPAddress(TraitTestBase):
     _good_values = [('localhost',0),('192.168.0.1',1000),('www.google.com',80)]
     _bad_values = [(0,0),('localhost',10.0),('localhost',-1), None]
 
+
 class ListTrait(HasTraits):
 
     value = List(Int())
 
+
 class TestList(TraitTestBase):
 
     obj = ListTrait()
@@ -1534,6 +1535,7 @@ class TestList(TraitTestBase):
             value = list(value)
         return value
 
+
 class Foo(object):
     pass
 
@@ -1653,10 +1655,42 @@ class TestMultiTuple(TraitTestBase):
     _good_values = [(1,b'a'), (2,b'b')]
     _bad_values = ((),10, b'a', (1,b'a',3), (b'a',1), (1, 'a'))
 
+
+@pytest.mark.parametrize(
+    "Trait", (List, Tuple, Set, Dict, Integer, Unicode,),
+)
+def test_allow_none_default_value(Trait):
+    class C(HasTraits):
+        t = Trait(default_value=None, allow_none=True)
+
+    # test default value
+    c = C()
+    assert c.t is None
+
+    # and in constructor
+    c = C(t=None)
+    assert c.t is None
+
+
+@pytest.mark.parametrize(
+    "Trait, default_value",
+    ((List, []), (Tuple, ()), (Set, set()), (Dict, {}), (Integer, 0), (Unicode, "")),
+)
+def test_default_value(Trait, default_value):
+    class C(HasTraits):
+        t = Trait()
+
+    # test default value
+    c = C()
+    assert type(c.t) is type(default_value)
+    assert c.t == default_value
+
+
 class CRegExpTrait(HasTraits):
 
     value = CRegExp(r'')
 
+
 class TestCRegExp(TraitTestBase):
 
     def coerce(self, value):