Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-105570: Deprecate unusual ways of creating empty TypedDicts #105780

Merged
merged 3 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,14 @@ These are not used in annotations. They are building blocks for declaring types.
.. versionchanged:: 3.13
Removed support for the keyword-argument method of creating ``TypedDict``\ s.

.. deprecated-removed:: 3.13 3.15
When using the functional syntax to create a TypedDict class, failing to
pass a value to the 'fields' parameter (``TD = TypedDict("TD")``) is
deprecated. Passing ``None`` to the 'fields' parameter
(``TD = TypedDict("TD", None)``) is also deprecated. Both will be
disallowed in Python 3.15. To create a TypedDict class with 0 fields,
use ``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``.

Protocols
---------

Expand Down
15 changes: 9 additions & 6 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,15 @@ Deprecated
be disallowed in Python 3.15. Use the class-based syntax or the functional
syntax instead. (Contributed by Alex Waygood in :gh:`105566`.)
* When using the functional syntax to create a :class:`typing.NamedTuple`
class, failing to pass a value to the 'fields' parameter
(``NT = NamedTuple("NT")``) is deprecated. Passing ``None`` to the 'fields'
parameter (``NT = NamedTuple("NT", None)``) is also deprecated. Both will be
disallowed in Python 3.15. To create a NamedTuple class with 0 fields, use
``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``.
(Contributed by Alex Waygood in :gh:`105566`.)
class or a :class:`typing.TypedDict` class, failing to pass a value to the
'fields' parameter (``NT = NamedTuple("NT")`` or ``TD = TypedDict("TD")``) is
deprecated. Passing ``None`` to the 'fields' parameter
(``NT = NamedTuple("NT", None)`` or ``TD = TypedDict("TD", None)``) is also
deprecated. Both will be disallowed in Python 3.15. To create a NamedTuple
class with 0 fields, use ``class NT(NamedTuple): pass`` or
``NT = NamedTuple("NT", [])``. To create a TypedDict class with 0 fields, use
``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``.
(Contributed by Alex Waygood in :gh:`105566` and :gh:`105570`.)

* :mod:`array`'s ``'u'`` format code, deprecated in docs since Python 3.3,
emits :exc:`DeprecationWarning` since 3.13
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7823,6 +7823,40 @@ class MultipleGenericBases(GenericParent[int], GenericParent[float]):
self.assertEqual(MultipleGenericBases.__orig_bases__, (GenericParent[int], GenericParent[float]))
self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,))

def test_zero_fields_typeddicts(self):
T1 = TypedDict("T1", {})
class T2(TypedDict): pass
class T3[tvar](TypedDict): pass
S = TypeVar("S")
class T4(TypedDict, Generic[S]): pass

expected_warning = re.escape(
"Failing to pass a value for the 'fields' parameter is deprecated "
"and will be disallowed in Python 3.15. "
"To create a TypedDict class with 0 fields "
"using the functional syntax, "
"pass an empty dictionary, e.g. `T5 = TypedDict('T5', {})`."
)
with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
T5 = TypedDict('T5')

expected_warning = re.escape(
"Passing `None` as the 'fields' parameter is deprecated "
"and will be disallowed in Python 3.15. "
"To create a TypedDict class with 0 fields "
"using the functional syntax, "
"pass an empty dictionary, e.g. `T6 = TypedDict('T6', {})`."
)
with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"):
T6 = TypedDict('T6', None)

for klass in T1, T2, T3, T4, T5, T6:
with self.subTest(klass=klass.__name__):
self.assertEqual(klass.__annotations__, {})
self.assertEqual(klass.__required_keys__, set())
self.assertEqual(klass.__optional_keys__, set())
self.assertIsInstance(klass(), dict)


class RequiredTests(BaseTestCase):

Expand Down
19 changes: 17 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2908,7 +2908,7 @@ def __subclasscheck__(cls, other):
__instancecheck__ = __subclasscheck__


def TypedDict(typename, fields=None, /, *, total=True):
def TypedDict(typename, fields=_sentinel, /, *, total=True):
"""A simple typed namespace. At runtime it is equivalent to a plain dict.

TypedDict creates a dictionary type such that a type checker will expect all
Expand Down Expand Up @@ -2955,7 +2955,22 @@ class Point2D(TypedDict):

See PEP 655 for more details on Required and NotRequired.
"""
if fields is None:
if fields is _sentinel or fields is None:
import warnings

if fields is _sentinel:
deprecated_thing = "Failing to pass a value for the 'fields' parameter"
else:
deprecated_thing = "Passing `None` as the 'fields' parameter"

example = f"`{typename} = TypedDict({typename!r}, {{{{}}}})`"
deprecation_msg = (
"{name} is deprecated and will be disallowed in Python {remove}. "
"To create a TypedDict class with 0 fields "
"using the functional syntax, "
"pass an empty dictionary, e.g. "
) + example + "."
warnings._deprecated(deprecated_thing, message=deprecation_msg, remove=(3, 15))
fields = {}

ns = {'__annotations__': dict(fields)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Deprecate two methods of creating :class:`typing.TypedDict` classes with 0
fields using the functional syntax: ``TD = TypedDict("TD")`` and
``TD = TypedDict("TD", None)``. Both will be disallowed in Python 3.15. To create a
``TypedDict`` class with 0 fields, either use ``class TD(TypedDict): pass``
or ``TD = TypedDict("TD", {})``.