Context: python/cpython#31781
Snippets below are unsupported and currently raise an error:
>>> from typing import NamedTuple
...
... class Foo(NamedTuple):
... x: int
...
... class Bar(NamedTuple, Foo):
... y: int
...
Traceback (most recent call last):
File "<python-input-0>", line 6, in <module>
class Bar(NamedTuple, Foo):
y: int
File "/Users/bartosz.slawecki/Python/cpython/Lib/typing.py", line 3021, in __new__
raise TypeError(
'can only inherit from a NamedTuple type and Generic')
TypeError: can only inherit from a NamedTuple type and Generic
>>> from typing import NamedTuple
...
... class Foo(NamedTuple):
... x: int
...
... class Bar(Foo, NamedTuple):
... y: int
...
Traceback (most recent call last):
File "<python-input-1>", line 6, in <module>
class Bar(Foo, NamedTuple):
y: int
File "/Users/bartosz.slawecki/Python/cpython/Lib/typing.py", line 3021, in __new__
raise TypeError(
'can only inherit from a NamedTuple type and Generic')
TypeError: can only inherit from a NamedTuple type and Generic
>>>with the change adding multiple inheritance support, the first snippet raises a less clear exception and the second silently passes:
>>> from typing import NamedTuple
...
... class Foo(NamedTuple):
... x: int
...
... class Bar(NamedTuple, Foo):
... y: int
...
Traceback (most recent call last):
File "<python-input-0>", line 6, in <module>
class Bar(NamedTuple, Foo):
y: int
File "/Users/bartosz.slawecki/Python/cpython/Lib/typing.py", line 3056, in __new__
nm_tpl.__bases__ = bases
^^^^^^^^^^^^^^^^
TypeError: Cannot create a consistent method resolution order (MRO) for bases tuple, Foo
>>> from typing import NamedTuple
...
... class Foo(NamedTuple):
... x: int
...
... class Bar(Foo, NamedTuple):
... y: int
...
>>>The latter is passing, so there is a chance of people starting to rely on it. It isn't easy to find a use case for utilizing this new possibility, but it has a number of issues:
-
super()is illegal in anytyping.NamedTuples, -
It creates a namedtuple from the annotations of the child class only:
>>> class Foo(NamedTuple): ... a: int ... b: int ... ... class Bar(Foo, NamedTuple): ... x: int ... y: int ... >>> Bar._fields ('x', 'y')
However, the fields of the parent
typing.NamedTupleare populated with fields of the child despite name mismatch. It is a reasonable expectation but a separate feature request to expect fields to be extended when aNamedTupleinherits aNamedTuple, but it is not supported and the actual implications of the second snippet are very uneasy to explain:>>> from typing import NamedTuple >>> >>> class BaseRecord(NamedTuple): ... id: int ... timestamp: float >>> >>> class LogEntry(BaseRecord, NamedTuple): ... level: str ... message: str >>> >>> entry = LogEntry(level="ERROR", message="disk full") >>> entry.id, entry.message ('ERROR', 'disk full') >>> entry.level, entry.timestamp ('ERROR', 'disk full')
The fact that
.idand.timestampfields (that there is no control over) are set as an unintended side effect of populating.leveland.messageis a bug, even though it is unknown how severe. I think it is better to continue preventing this specific case from being possible to execute at runtime.
We could document that making a typing.NamedTuple inheriting typing.NamedTuple is undefined behavior or consider identifying such calls and make both raise an exception:
>>> from typing import NamedTuple
...
... class Foo(NamedTuple):
... x: int
...
... class Bar(NamedTuple, Foo):
... y: int
...
Traceback (most recent call last):
...
TypeError: a NamedTuple cannot inherit from a NamedTuple
>>> from typing import NamedTuple
...
... class Foo(NamedTuple):
... x: int
...
... class Bar(Foo, NamedTuple):
... y: int
...
Traceback (most recent call last):
...
TypeError: a NamedTuple cannot inherit from a NamedTupleRight now, it could be implemented by rejecting any base whose .__orig_bases__ contain the NamedTuple function.
Relying on .__orig_bases__ could be brittle, because that attribute can be removed,
and the NamedTuple function is recognized by identity, but it might be a good enough heuristic.