Skip to content

Commit f6f36cc

Browse files
sransaraFidget-SpinneruriyyoJelleZijlstraserhiy-storchaka
authored
bpo-44863: Allow generic typing.TypedDict (#27663)
Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Co-authored-by: Yurii Karabas <1998uriyyo@gmail.com> Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 6c7249f commit f6f36cc

File tree

6 files changed

+172
-7
lines changed

6 files changed

+172
-7
lines changed

Doc/library/typing.rst

+10-1
Original file line numberDiff line numberDiff line change
@@ -1738,7 +1738,7 @@ These are not used in annotations. They are building blocks for declaring types.
17381738
z: int
17391739

17401740
A ``TypedDict`` cannot inherit from a non-TypedDict class,
1741-
notably including :class:`Generic`. For example::
1741+
except for :class:`Generic`. For example::
17421742

17431743
class X(TypedDict):
17441744
x: int
@@ -1755,6 +1755,12 @@ These are not used in annotations. They are building blocks for declaring types.
17551755
T = TypeVar('T')
17561756
class XT(X, Generic[T]): pass # raises TypeError
17571757

1758+
A ``TypedDict`` can be generic::
1759+
1760+
class Group(TypedDict, Generic[T]):
1761+
key: T
1762+
group: list[T]
1763+
17581764
A ``TypedDict`` can be introspected via annotations dicts
17591765
(see :ref:`annotations-howto` for more information on annotations best practices),
17601766
:attr:`__total__`, :attr:`__required_keys__`, and :attr:`__optional_keys__`.
@@ -1802,6 +1808,9 @@ These are not used in annotations. They are building blocks for declaring types.
18021808

18031809
.. versionadded:: 3.8
18041810

1811+
.. versionchanged:: 3.11
1812+
Added support for generic ``TypedDict``\ s.
1813+
18051814
Generic concrete collections
18061815
----------------------------
18071816

Doc/whatsnew/3.11.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,10 @@ For major changes, see :ref:`new-feat-related-type-hints-311`.
715715
to clear all registered overloads of a function.
716716
(Contributed by Jelle Zijlstra in :gh:`89263`.)
717717

718-
* :class:`~typing.NamedTuple` subclasses can be generic.
718+
* :data:`typing.TypedDict` subclasses can now be generic. (Contributed by
719+
Samodya Abey in :gh:`89026`.)
720+
721+
* :class:`~typing.NamedTuple` subclasses can now be generic.
719722
(Contributed by Serhiy Storchaka in :issue:`43923`.)
720723

721724

Lib/test/_typed_dict_helper.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ class Bar(_typed_dict_helper.Foo, total=False):
1313

1414
from __future__ import annotations
1515

16-
from typing import Annotated, Optional, Required, TypedDict
16+
from typing import Annotated, Generic, Optional, Required, TypedDict, TypeVar
17+
1718

1819
OptionalIntType = Optional[int]
1920

2021
class Foo(TypedDict):
2122
a: OptionalIntType
2223

24+
T = TypeVar("T")
25+
26+
class FooGeneric(TypedDict, Generic[T]):
27+
a: Optional[T]
28+
2329
class VeryAnnotated(TypedDict, total=False):
2430
a: Annotated[Annotated[Annotated[Required[int], "a"], "b"], "c"]

Lib/test/test_typing.py

+136
Original file line numberDiff line numberDiff line change
@@ -4530,9 +4530,16 @@ class Point2D(TypedDict):
45304530
x: int
45314531
y: int
45324532

4533+
class Point2DGeneric(Generic[T], TypedDict):
4534+
a: T
4535+
b: T
4536+
45334537
class Bar(_typed_dict_helper.Foo, total=False):
45344538
b: int
45354539

4540+
class BarGeneric(_typed_dict_helper.FooGeneric[T], total=False):
4541+
b: int
4542+
45364543
class LabelPoint2D(Point2D, Label): ...
45374544

45384545
class Options(TypedDict, total=False):
@@ -5890,6 +5897,17 @@ def test_pickle(self):
58905897
EmpDnew = pickle.loads(ZZ)
58915898
self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane)
58925899

5900+
def test_pickle_generic(self):
5901+
point = Point2DGeneric(a=5.0, b=3.0)
5902+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
5903+
z = pickle.dumps(point, proto)
5904+
point2 = pickle.loads(z)
5905+
self.assertEqual(point2, point)
5906+
self.assertEqual(point2, {'a': 5.0, 'b': 3.0})
5907+
ZZ = pickle.dumps(Point2DGeneric, proto)
5908+
Point2DGenericNew = pickle.loads(ZZ)
5909+
self.assertEqual(Point2DGenericNew({'a': 5.0, 'b': 3.0}), point)
5910+
58935911
def test_optional(self):
58945912
EmpD = TypedDict('EmpD', {'name': str, 'id': int})
58955913

@@ -6074,6 +6092,124 @@ def test_get_type_hints(self):
60746092
{'a': typing.Optional[int], 'b': int}
60756093
)
60766094

6095+
def test_get_type_hints_generic(self):
6096+
self.assertEqual(
6097+
get_type_hints(BarGeneric),
6098+
{'a': typing.Optional[T], 'b': int}
6099+
)
6100+
6101+
class FooBarGeneric(BarGeneric[int]):
6102+
c: str
6103+
6104+
self.assertEqual(
6105+
get_type_hints(FooBarGeneric),
6106+
{'a': typing.Optional[T], 'b': int, 'c': str}
6107+
)
6108+
6109+
def test_generic_inheritance(self):
6110+
class A(TypedDict, Generic[T]):
6111+
a: T
6112+
6113+
self.assertEqual(A.__bases__, (Generic, dict))
6114+
self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T]))
6115+
self.assertEqual(A.__mro__, (A, Generic, dict, object))
6116+
self.assertEqual(A.__parameters__, (T,))
6117+
self.assertEqual(A[str].__parameters__, ())
6118+
self.assertEqual(A[str].__args__, (str,))
6119+
6120+
class A2(Generic[T], TypedDict):
6121+
a: T
6122+
6123+
self.assertEqual(A2.__bases__, (Generic, dict))
6124+
self.assertEqual(A2.__orig_bases__, (Generic[T], TypedDict))
6125+
self.assertEqual(A2.__mro__, (A2, Generic, dict, object))
6126+
self.assertEqual(A2.__parameters__, (T,))
6127+
self.assertEqual(A2[str].__parameters__, ())
6128+
self.assertEqual(A2[str].__args__, (str,))
6129+
6130+
class B(A[KT], total=False):
6131+
b: KT
6132+
6133+
self.assertEqual(B.__bases__, (Generic, dict))
6134+
self.assertEqual(B.__orig_bases__, (A[KT],))
6135+
self.assertEqual(B.__mro__, (B, Generic, dict, object))
6136+
self.assertEqual(B.__parameters__, (KT,))
6137+
self.assertEqual(B.__total__, False)
6138+
self.assertEqual(B.__optional_keys__, frozenset(['b']))
6139+
self.assertEqual(B.__required_keys__, frozenset(['a']))
6140+
6141+
self.assertEqual(B[str].__parameters__, ())
6142+
self.assertEqual(B[str].__args__, (str,))
6143+
self.assertEqual(B[str].__origin__, B)
6144+
6145+
class C(B[int]):
6146+
c: int
6147+
6148+
self.assertEqual(C.__bases__, (Generic, dict))
6149+
self.assertEqual(C.__orig_bases__, (B[int],))
6150+
self.assertEqual(C.__mro__, (C, Generic, dict, object))
6151+
self.assertEqual(C.__parameters__, ())
6152+
self.assertEqual(C.__total__, True)
6153+
self.assertEqual(C.__optional_keys__, frozenset(['b']))
6154+
self.assertEqual(C.__required_keys__, frozenset(['a', 'c']))
6155+
assert C.__annotations__ == {
6156+
'a': T,
6157+
'b': KT,
6158+
'c': int,
6159+
}
6160+
with self.assertRaises(TypeError):
6161+
C[str]
6162+
6163+
6164+
class Point3D(Point2DGeneric[T], Generic[T, KT]):
6165+
c: KT
6166+
6167+
self.assertEqual(Point3D.__bases__, (Generic, dict))
6168+
self.assertEqual(Point3D.__orig_bases__, (Point2DGeneric[T], Generic[T, KT]))
6169+
self.assertEqual(Point3D.__mro__, (Point3D, Generic, dict, object))
6170+
self.assertEqual(Point3D.__parameters__, (T, KT))
6171+
self.assertEqual(Point3D.__total__, True)
6172+
self.assertEqual(Point3D.__optional_keys__, frozenset())
6173+
self.assertEqual(Point3D.__required_keys__, frozenset(['a', 'b', 'c']))
6174+
assert Point3D.__annotations__ == {
6175+
'a': T,
6176+
'b': T,
6177+
'c': KT,
6178+
}
6179+
self.assertEqual(Point3D[int, str].__origin__, Point3D)
6180+
6181+
with self.assertRaises(TypeError):
6182+
Point3D[int]
6183+
6184+
with self.assertRaises(TypeError):
6185+
class Point3D(Point2DGeneric[T], Generic[KT]):
6186+
c: KT
6187+
6188+
def test_implicit_any_inheritance(self):
6189+
class A(TypedDict, Generic[T]):
6190+
a: T
6191+
6192+
class B(A[KT], total=False):
6193+
b: KT
6194+
6195+
class WithImplicitAny(B):
6196+
c: int
6197+
6198+
self.assertEqual(WithImplicitAny.__bases__, (Generic, dict,))
6199+
self.assertEqual(WithImplicitAny.__mro__, (WithImplicitAny, Generic, dict, object))
6200+
# Consistent with GenericTests.test_implicit_any
6201+
self.assertEqual(WithImplicitAny.__parameters__, ())
6202+
self.assertEqual(WithImplicitAny.__total__, True)
6203+
self.assertEqual(WithImplicitAny.__optional_keys__, frozenset(['b']))
6204+
self.assertEqual(WithImplicitAny.__required_keys__, frozenset(['a', 'c']))
6205+
assert WithImplicitAny.__annotations__ == {
6206+
'a': T,
6207+
'b': KT,
6208+
'c': int,
6209+
}
6210+
with self.assertRaises(TypeError):
6211+
WithImplicitAny[str]
6212+
60776213
def test_non_generic_subscript(self):
60786214
# For backward compatibility, subscription works
60796215
# on arbitrary TypedDict types.

Lib/typing.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,9 @@ def __init_subclass__(cls, *args, **kwargs):
17961796
if '__orig_bases__' in cls.__dict__:
17971797
error = Generic in cls.__orig_bases__
17981798
else:
1799-
error = Generic in cls.__bases__ and cls.__name__ != 'Protocol'
1799+
error = (Generic in cls.__bases__ and
1800+
cls.__name__ != 'Protocol' and
1801+
type(cls) != _TypedDictMeta)
18001802
if error:
18011803
raise TypeError("Cannot inherit from plain Generic")
18021804
if '__orig_bases__' in cls.__dict__:
@@ -2868,14 +2870,19 @@ def __new__(cls, name, bases, ns, total=True):
28682870
Subclasses and instances of TypedDict return actual dictionaries.
28692871
"""
28702872
for base in bases:
2871-
if type(base) is not _TypedDictMeta:
2873+
if type(base) is not _TypedDictMeta and base is not Generic:
28722874
raise TypeError('cannot inherit from both a TypedDict type '
28732875
'and a non-TypedDict base class')
2874-
tp_dict = type.__new__(_TypedDictMeta, name, (dict,), ns)
2876+
2877+
if any(issubclass(b, Generic) for b in bases):
2878+
generic_base = (Generic,)
2879+
else:
2880+
generic_base = ()
2881+
2882+
tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict), ns)
28752883

28762884
annotations = {}
28772885
own_annotations = ns.get('__annotations__', {})
2878-
own_annotation_keys = set(own_annotations.keys())
28792886
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
28802887
own_annotations = {
28812888
n: _type_check(tp, msg, module=tp_dict.__module__)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Allow :class:`~typing.TypedDict` subclasses to also include
2+
:class:`~typing.Generic` as a base class in class based syntax. Thereby allowing
3+
the user to define a generic ``TypedDict``, just like a user-defined generic but
4+
with ``TypedDict`` semantics.

0 commit comments

Comments
 (0)