Skip to content

Commit b95e923

Browse files
Zac-HDJake Taylor
authored andcommitted
Better runtime TypedDict (pythonGH-17214)
This patch enables downstream projects inspecting a TypedDict subclass at runtime to tell which keys are optional. This is essential for generating test data with Hypothesis or validating inputs with typeguard or pydantic.
1 parent 5b00c6d commit b95e923

File tree

3 files changed

+25
-3
lines changed

3 files changed

+25
-3
lines changed

Lib/test/test_typing.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3741,6 +3741,13 @@ def test_total(self):
37413741
self.assertEqual(Options(log_level=2), {'log_level': 2})
37423742
self.assertEqual(Options.__total__, False)
37433743

3744+
def test_optional_keys(self):
3745+
class Point2Dor3D(Point2D, total=False):
3746+
z: int
3747+
3748+
assert Point2Dor3D.__required_keys__ == frozenset(['x', 'y'])
3749+
assert Point2Dor3D.__optional_keys__ == frozenset(['z'])
3750+
37443751

37453752
class IOTests(BaseTestCase):
37463753

Lib/typing.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,9 +1715,20 @@ def __new__(cls, name, bases, ns, total=True):
17151715
anns = ns.get('__annotations__', {})
17161716
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
17171717
anns = {n: _type_check(tp, msg) for n, tp in anns.items()}
1718+
required = set(anns if total else ())
1719+
optional = set(() if total else anns)
1720+
17181721
for base in bases:
1719-
anns.update(base.__dict__.get('__annotations__', {}))
1722+
base_anns = base.__dict__.get('__annotations__', {})
1723+
anns.update(base_anns)
1724+
if getattr(base, '__total__', True):
1725+
required.update(base_anns)
1726+
else:
1727+
optional.update(base_anns)
1728+
17201729
tp_dict.__annotations__ = anns
1730+
tp_dict.__required_keys__ = frozenset(required)
1731+
tp_dict.__optional_keys__ = frozenset(optional)
17211732
if not hasattr(tp_dict, '__total__'):
17221733
tp_dict.__total__ = total
17231734
return tp_dict
@@ -1744,8 +1755,9 @@ class Point2D(TypedDict):
17441755
17451756
assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')
17461757
1747-
The type info can be accessed via Point2D.__annotations__. TypedDict
1748-
supports two additional equivalent forms::
1758+
The type info can be accessed via the Point2D.__annotations__ dict, and
1759+
the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets.
1760+
TypedDict supports two additional equivalent forms::
17491761
17501762
Point2D = TypedDict('Point2D', x=int, y=int, label=str)
17511763
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`typing.TypedDict` subclasses now track which keys are optional using
2+
the ``__required_keys__`` and ``__optional_keys__`` attributes, to enable
3+
runtime validation by downstream projects. Patch by Zac Hatfield-Dodds.

0 commit comments

Comments
 (0)