Skip to content

gh-133823: require explicit empty sequence for 0-field TypedDict objects #133863

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

Merged
merged 4 commits into from
May 11, 2025
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
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ typing
Use the class-based syntax or the functional syntax instead.
(Contributed by Bénédikt Tran in :gh:`133817`.)

* Using ``TD = TypedDict("TD")`` or ``TD = TypedDict("TD", None)`` to
construct a :class:`~typing.TypedDict` type with zero field is no
longer supported. Use ``class TD(TypedDict): pass``
or ``TD = TypedDict("TD", {})`` instead.
(Contributed by Bénédikt Tran in :gh:`133823`.)


Porting to Python 3.15
======================
Expand Down
32 changes: 10 additions & 22 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8853,39 +8853,27 @@ class MultipleGenericBases(GenericParent[int], GenericParent[float]):
self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,))

def test_zero_fields_typeddicts(self):
T1 = TypedDict("T1", {})
T1a = TypedDict("T1a", {})
T1b = TypedDict("T1b", [])
T1c = TypedDict("T1c", ())
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:
for klass in T1a, T1b, T1c, T2, T3, T4:
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)

def test_errors(self):
with self.assertRaisesRegex(TypeError, "missing 1 required.*argument"):
TypedDict('TD')
with self.assertRaisesRegex(TypeError, "object is not iterable"):
TypedDict('TD', None)

def test_readonly_inheritance(self):
class Base1(TypedDict):
a: ReadOnly[int]
Expand Down
20 changes: 1 addition & 19 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3159,7 +3159,7 @@ def __subclasscheck__(cls, other):
__instancecheck__ = __subclasscheck__


def TypedDict(typename, fields=_sentinel, /, *, total=True):
def TypedDict(typename, fields, /, *, 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 @@ -3214,24 +3214,6 @@ class DatabaseUser(TypedDict):
username: str # the "username" key can be changed

"""
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)}
module = _caller()
if module is not None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Remove support for ``TD = TypedDict("TD")`` and ``TD = TypedDict("TD", None)``
calls for constructing :class:`typing.TypedDict` objects with zero field.
Patch by Bénédikt Tran.
Loading