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
Changes from 1 commit
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
9 changes: 6 additions & 3 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
@@ -118,10 +118,13 @@ Deprecated
Removed
=======

module_name
-----------
typing
------

* TODO
* Using ``TypedDict("T")`` or ``TypedDict("T", None)`` to construct
:class:`~typing.TypedDict` with zero fields is no more supported.
Use ``TypedDict("T", [])`` instead.
(Contributed by Bénédikt Tran in :gh:`133823`.)


Porting to Python 3.15
32 changes: 10 additions & 22 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
@@ -8904,39 +8904,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]
20 changes: 1 addition & 19 deletions Lib/typing.py
Original file line number Diff line number Diff line change
@@ -3198,7 +3198,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
@@ -3253,24 +3253,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:
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Remove support for ``TypedDict("T")`` and ``TypedDict("T", None)`` calls for
constructing :class:`typing.TypedDict` objects with zero fields. Patch by
Bénédikt Tran.
Loading
Oops, something went wrong.