Related: python/typing#427
So, all classes like Bar
from
from typing import NamedTuple
class Foo(NamedTuple):
pass
class Bar(Foo):
pass
should be illegal.
Supporting them was never explicitly intended nor documented.
While they look like they inherit structure, they don’t inherit behavior—leading to broken expectations around things like constructors and super()
.
There’s no clear, compelling use case that isn’t better served by dataclasses or other tools.
I expect this experiment to end up in
- the class statement with inheritance from a typed namedtuple to fail with a
TypeError
("can't inherit from typed named tuples") - a clear requirement in the typing specification for type checkers to understand typed namedtuples as
@final
implicitly
typing.NamedTuple
subclasses' subclasses support super()
, but bare typing.NamedTuple
subclasses don't.
You can do
from typing import NamedTuple
class Foo(NamedTuple):
pass
class Bar(Foo):
def biz(self) -> None:
super()
but you can't do
from typing import NamedTuple
class Foo(NamedTuple):
def bar(self) -> None:
super()
As a Python user, for
from typing import NamedTuple
class Point2D(NamedTuple):
x: int
y: int
class Point3D(Point2D):
z: int
I'd typically expect Point3D
to extend Point2D
with a new field z
.
Right?
The same mechanism works in Pydantic, dataclasses, and attrs. These libraries become more and more present in PEPs1, so yes, general patterns from them are worth consideration.
Most relevant, because it has class-based API:
from pydantic import BaseModel
class Foo(BaseModel):
x: int
class Bar(Foo):
y: int
print(inspect.signature(Bar))
# (*, x: int, y: int) -> None
(standard library)
@dataclass
class Foo:
x: int
@dataclass # if omitted, pure inheritance does nothing with the subclass
class Bar(Foo):
y: int
print(inspect.signature(Bar))
# (x: int, y: int) -> None
from attrs import define
@define()
class Foo:
x: int
@define() # if omitted, pure inheritance does nothing with the subclass
class Bar(Foo):
y: int
print(inspect.signature(Bar))
# (x: int, y: int) -> None
Now, for
from typing import NamedTuple
class Foo(NamedTuple):
x: int
class Bar(Foo):
y: int
What's the expected constructor of Bar
?
(x: int, y: int)
, (x: int)
or (y: int)
?
At runtime, it's (x: int)
:
import inspect
print(inspect.signature(Bar))
That's because:
-
it's inherently unsupported to reuse
typing.NamedTuple
logic on such a subclass, because typed named tuples can't be mixed with arbitrary bases (python/cpython#116241):from typing import NamedTuple, NamedTupleMeta class Foo(NamedTuple): x: int class Bar(Foo, metaclass=NamedTupleMeta): y: int
Traceback (assertions disabled)
Traceback (most recent call last): File "/home/bswck/Python/cpython/t.py", line 6, in <module> class Bar(Foo, metaclass=NamedTupleMeta): y: int File "/home/bswck/Python/cpython/Lib/typing.py", line 2889, in __new__ raise TypeError( 'can only inherit from a NamedTuple type and Generic') TypeError: can only inherit from a NamedTuple type and Generic
-
the metaclass of any direct
typing.NamedTuple
subclass istype
, nottyping.NamedTupleMeta
:from typing import NamedTuple, NamedTupleMeta class Foo(NamedTuple): x: int class Bar(Foo): y: int print(type(Bar)) # <class 'type'>
which is correct and expected, because
Foo
is a namedtuple (and its metaclass istype
as well).
They may have useful insights, especially those who contributed to python/cpython#72742.
Do you remember if it was intended to support subclasses of typing.NamedTuple
"subclasses"?
- Jelle Zijlstra: I don't think it was
https://justforfunnoreally.dev/