Skip to content

Commit b04e02c

Browse files
bpo-43923: Add support for generic typing.NamedTuple (#92027)
1 parent 81fb354 commit b04e02c

File tree

5 files changed

+66
-4
lines changed

5 files changed

+66
-4
lines changed

Doc/library/typing.rst

+9
Original file line numberDiff line numberDiff line change
@@ -1615,6 +1615,12 @@ These are not used in annotations. They are building blocks for declaring types.
16151615
def __repr__(self) -> str:
16161616
return f'<Employee {self.name}, id={self.id}>'
16171617

1618+
``NamedTuple`` subclasses can be generic::
1619+
1620+
class Group(NamedTuple, Generic[T]):
1621+
key: T
1622+
group: list[T]
1623+
16181624
Backward-compatible usage::
16191625

16201626
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
@@ -1633,6 +1639,9 @@ These are not used in annotations. They are building blocks for declaring types.
16331639
Removed the ``_field_types`` attribute in favor of the more
16341640
standard ``__annotations__`` attribute which has the same information.
16351641

1642+
.. versionchanged:: 3.11
1643+
Added support for generic namedtuples.
1644+
16361645
.. class:: NewType(name, tp)
16371646

16381647
A helper class to indicate a distinct type to a typechecker,

Doc/whatsnew/3.11.rst

+4
Original file line numberDiff line numberDiff line change
@@ -715,6 +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.
719+
(Contributed by Serhiy Storchaka in :issue:`43923`.)
720+
721+
718722
unicodedata
719723
-----------
720724

Lib/test/test_typing.py

+39
Original file line numberDiff line numberDiff line change
@@ -5678,6 +5678,45 @@ class A:
56785678
with self.assertRaises(TypeError):
56795679
class X(NamedTuple, A):
56805680
x: int
5681+
with self.assertRaises(TypeError):
5682+
class X(NamedTuple, tuple):
5683+
x: int
5684+
with self.assertRaises(TypeError):
5685+
class X(NamedTuple, NamedTuple):
5686+
x: int
5687+
class A(NamedTuple):
5688+
x: int
5689+
with self.assertRaises(TypeError):
5690+
class X(NamedTuple, A):
5691+
y: str
5692+
5693+
def test_generic(self):
5694+
class X(NamedTuple, Generic[T]):
5695+
x: T
5696+
self.assertEqual(X.__bases__, (tuple, Generic))
5697+
self.assertEqual(X.__orig_bases__, (NamedTuple, Generic[T]))
5698+
self.assertEqual(X.__mro__, (X, tuple, Generic, object))
5699+
5700+
class Y(Generic[T], NamedTuple):
5701+
x: T
5702+
self.assertEqual(Y.__bases__, (Generic, tuple))
5703+
self.assertEqual(Y.__orig_bases__, (Generic[T], NamedTuple))
5704+
self.assertEqual(Y.__mro__, (Y, Generic, tuple, object))
5705+
5706+
for G in X, Y:
5707+
with self.subTest(type=G):
5708+
self.assertEqual(G.__parameters__, (T,))
5709+
A = G[int]
5710+
self.assertIs(A.__origin__, G)
5711+
self.assertEqual(A.__args__, (int,))
5712+
self.assertEqual(A.__parameters__, ())
5713+
5714+
a = A(3)
5715+
self.assertIs(type(a), G)
5716+
self.assertEqual(a.x, 3)
5717+
5718+
with self.assertRaises(TypeError):
5719+
G[int, str]
56815720

56825721
def test_namedtuple_keyword_usage(self):
56835722
LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)

Lib/typing.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -2764,7 +2764,12 @@ def _make_nmtuple(name, types, module, defaults = ()):
27642764
class NamedTupleMeta(type):
27652765

27662766
def __new__(cls, typename, bases, ns):
2767-
assert bases[0] is _NamedTuple
2767+
assert _NamedTuple in bases
2768+
for base in bases:
2769+
if base is not _NamedTuple and base is not Generic:
2770+
raise TypeError(
2771+
'can only inherit from a NamedTuple type and Generic')
2772+
bases = tuple(tuple if base is _NamedTuple else base for base in bases)
27682773
types = ns.get('__annotations__', {})
27692774
default_names = []
27702775
for field_name in types:
@@ -2778,12 +2783,18 @@ def __new__(cls, typename, bases, ns):
27782783
nm_tpl = _make_nmtuple(typename, types.items(),
27792784
defaults=[ns[n] for n in default_names],
27802785
module=ns['__module__'])
2786+
nm_tpl.__bases__ = bases
2787+
if Generic in bases:
2788+
class_getitem = Generic.__class_getitem__.__func__
2789+
nm_tpl.__class_getitem__ = classmethod(class_getitem)
27812790
# update from user namespace without overriding special namedtuple attributes
27822791
for key in ns:
27832792
if key in _prohibited:
27842793
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
27852794
elif key not in _special and key not in nm_tpl._fields:
27862795
setattr(nm_tpl, key, ns[key])
2796+
if Generic in bases:
2797+
nm_tpl.__init_subclass__()
27872798
return nm_tpl
27882799

27892800

@@ -2821,9 +2832,7 @@ class Employee(NamedTuple):
28212832
_NamedTuple = type.__new__(NamedTupleMeta, 'NamedTuple', (), {})
28222833

28232834
def _namedtuple_mro_entries(bases):
2824-
if len(bases) > 1:
2825-
raise TypeError("Multiple inheritance with NamedTuple is not supported")
2826-
assert bases[0] is NamedTuple
2835+
assert NamedTuple in bases
28272836
return (_NamedTuple,)
28282837

28292838
NamedTuple.__mro_entries__ = _namedtuple_mro_entries
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for generic :class:`typing.NamedTuple`.

0 commit comments

Comments
 (0)