bpo-46066: Deprecate kwargs syntax for TypedDict definitions (GH-31126)

Closes python/typing#981

https://bugs.python.org/issue46066
This commit is contained in:
97littleleaf11 2022-02-17 11:26:07 +08:00 committed by GitHub
parent 6f1efd19a7
commit de6043e596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 35 additions and 10 deletions

View File

@ -1470,10 +1470,19 @@ These are not used in annotations. They are building blocks for declaring types.
``Point2D.__optional_keys__``. ``Point2D.__optional_keys__``.
To allow using this feature with older versions of Python that do not To allow using this feature with older versions of Python that do not
support :pep:`526`, ``TypedDict`` supports two additional equivalent support :pep:`526`, ``TypedDict`` supports two additional equivalent
syntactic forms:: syntactic forms:
* Using a literal :class:`dict` as the second argument::
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
* Using keyword arguments::
Point2D = TypedDict('Point2D', x=int, y=int, label=str) Point2D = TypedDict('Point2D', x=int, y=int, label=str)
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
.. deprecated-removed:: 3.11 3.13
The keyword-argument syntax is deprecated in 3.11 and will be removed
in 3.13. It may also be unsupported by static type checkers.
By default, all keys must be present in a ``TypedDict``. It is possible to By default, all keys must be present in a ``TypedDict``. It is possible to
override this by specifying totality. override this by specifying totality.
@ -1483,6 +1492,9 @@ These are not used in annotations. They are building blocks for declaring types.
x: int x: int
y: int y: int
# Alternative syntax
Point2D = TypedDict('Point2D', {'x': int, 'y': int}, total=False)
This means that a ``Point2D`` ``TypedDict`` can have any of the keys This means that a ``Point2D`` ``TypedDict`` can have any of the keys
omitted. A type checker is only expected to support a literal ``False`` or omitted. A type checker is only expected to support a literal ``False`` or
``True`` as the value of the ``total`` argument. ``True`` is the default, ``True`` as the value of the ``total`` argument. ``True`` is the default,

View File

@ -4380,7 +4380,8 @@ class TypedDictTests(BaseTestCase):
self.assertEqual(Emp.__total__, True) self.assertEqual(Emp.__total__, True)
def test_basics_keywords_syntax(self): def test_basics_keywords_syntax(self):
Emp = TypedDict('Emp', name=str, id=int) with self.assertWarns(DeprecationWarning):
Emp = TypedDict('Emp', name=str, id=int)
self.assertIsSubclass(Emp, dict) self.assertIsSubclass(Emp, dict)
self.assertIsSubclass(Emp, typing.MutableMapping) self.assertIsSubclass(Emp, typing.MutableMapping)
self.assertNotIsSubclass(Emp, collections.abc.Sequence) self.assertNotIsSubclass(Emp, collections.abc.Sequence)
@ -4395,7 +4396,8 @@ class TypedDictTests(BaseTestCase):
self.assertEqual(Emp.__total__, True) self.assertEqual(Emp.__total__, True)
def test_typeddict_special_keyword_names(self): def test_typeddict_special_keyword_names(self):
TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, fields=list, _fields=dict) with self.assertWarns(DeprecationWarning):
TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, fields=list, _fields=dict)
self.assertEqual(TD.__name__, 'TD') self.assertEqual(TD.__name__, 'TD')
self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, '_typename': int, 'fields': list, '_fields': dict}) self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, '_typename': int, 'fields': list, '_fields': dict})
a = TD(cls=str, self=42, typename='foo', _typename=53, fields=[('bar', tuple)], _fields={'baz', set}) a = TD(cls=str, self=42, typename='foo', _typename=53, fields=[('bar', tuple)], _fields={'baz', set})
@ -4451,7 +4453,7 @@ class TypedDictTests(BaseTestCase):
def test_pickle(self): def test_pickle(self):
global EmpD # pickle wants to reference the class by name global EmpD # pickle wants to reference the class by name
EmpD = TypedDict('EmpD', name=str, id=int) EmpD = TypedDict('EmpD', {'name': str, 'id': int})
jane = EmpD({'name': 'jane', 'id': 37}) jane = EmpD({'name': 'jane', 'id': 37})
for proto in range(pickle.HIGHEST_PROTOCOL + 1): for proto in range(pickle.HIGHEST_PROTOCOL + 1):
z = pickle.dumps(jane, proto) z = pickle.dumps(jane, proto)
@ -4463,7 +4465,7 @@ class TypedDictTests(BaseTestCase):
self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane) self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane)
def test_optional(self): def test_optional(self):
EmpD = TypedDict('EmpD', name=str, id=int) EmpD = TypedDict('EmpD', {'name': str, 'id': int})
self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD]) self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD])
self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD]) self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD])

View File

@ -2569,9 +2569,8 @@ def TypedDict(typename, fields=None, /, *, total=True, **kwargs):
The type info can be accessed via the Point2D.__annotations__ dict, and The type info can be accessed via the Point2D.__annotations__ dict, and
the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets.
TypedDict supports two additional equivalent forms:: TypedDict supports an additional equivalent form::
Point2D = TypedDict('Point2D', x=int, y=int, label=str)
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
By default, all keys must be present in a TypedDict. It is possible By default, all keys must be present in a TypedDict. It is possible
@ -2587,14 +2586,22 @@ def TypedDict(typename, fields=None, /, *, total=True, **kwargs):
the total argument. True is the default, and makes all items defined in the the total argument. True is the default, and makes all items defined in the
class body be required. class body be required.
The class syntax is only supported in Python 3.6+, while two other The class syntax is only supported in Python 3.6+, while the other
syntax forms work for Python 2.7 and 3.2+ syntax form works for Python 2.7 and 3.2+
""" """
if fields is None: if fields is None:
fields = kwargs fields = kwargs
elif kwargs: elif kwargs:
raise TypeError("TypedDict takes either a dict or keyword arguments," raise TypeError("TypedDict takes either a dict or keyword arguments,"
" but not both") " but not both")
if kwargs:
warnings.warn(
"The kwargs-based syntax for TypedDict definitions is deprecated "
"in Python 3.11, will be removed in Python 3.13, and may not be "
"understood by third-party type checkers.",
DeprecationWarning,
stacklevel=2,
)
ns = {'__annotations__': dict(fields)} ns = {'__annotations__': dict(fields)}
module = _caller() module = _caller()

View File

@ -1974,6 +1974,7 @@ Arnon Yaari
Alakshendra Yadav Alakshendra Yadav
Hirokazu Yamamoto Hirokazu Yamamoto
Masayuki Yamamoto Masayuki Yamamoto
Jingchen Ye
Ka-Ping Yee Ka-Ping Yee
Chi Hsuan Yen Chi Hsuan Yen
Jason Yeo Jason Yeo

View File

@ -0,0 +1,3 @@
Deprecate kwargs-based syntax for :class:`typing.TypedDict` definitions.
It had confusing semantics when specifying totality, and was largely unused.
Patch by Jingchen Ye.