Make `typing.NamedTuple` classes final

This proposal aims to make typing.NamedTuple classes final—disallowing subclassing—in order to eliminate long-standing inconsistencies, surprising runtime behavior, and broken inheritance semantics.

Consider the following example:

from typing import NamedTuple

class Point2D(NamedTuple):
    x: int
    y: int

class Point3D(Point2D):
    z: int

A reasonable expectation is that the constructor signature for Point3D would be:

(x: int, y: int, z: int)

However, the actual constructor signature is:

(x: int, y: int)

The z annotation in the subclass does not add a new field. This is because subclasses of typing.NamedTuple classes are not reprocessed to include additional annotations—unlike similar annotation-based data structures in dataclasses, attrs, or Pydantic.

Comparison with other tools

Other popular Python tools do support extending fields via subclass annotations:

Examples

Pydantic

import inspect
from pydantic import BaseModel

class Point2D(BaseModel):
    x: int
    y: int

class Point3D(Point2D):  # Semantics stem from inheritance 
    z: int

print(inspect.signature(Point3D))
# (*, x: int, y: int, z: int) -> None

Dataclasses (stdlib)

import inspect
from dataclasses import dataclass

@dataclass
class Point2D:
    x: int
    y: int

@dataclass  # The decorator reapplies semantics respecting inheritance
class Point3D(Point2D):
    z: int

print(inspect.signature(Point3D))
# (x: int, y: int, z: int) -> None

attrs

import inspect
from attrs import define

@define
class Point2D:
    x: int
    y: int

@define  # The decorator reapplies semantics respecting inheritance
class Point3D(Point2D):
    z: int

print(inspect.signature(Point3D))
# (x: int, y: int, z: int) -> None

In contrast, typing.NamedTuple does not support this pattern—and is unlikely to change due to deep technical constraints and lack of compelling use cases that are not better served by other tools.

The problem

typing.NamedTuple breaks the core inheritance mechanisms of Python.

Subclassing typing.NamedTuple classes creates the illusion of behavior similar to that in other tools, but:

  • The constructor is not updated to include new fields.
  • super() does not work in methods of typing.NamedTuple classes.

Example: super() behavior

from typing import NamedTuple

class Foo(NamedTuple):
    pass

class Bar(Foo):
    def biz(self) -> None:
        super()  # Works, delegates to tuple or object

But:

from typing import NamedTuple

class Foo(NamedTuple):
    def bar(self) -> None:
        super()  # Raises at runtime

In Python ≤ 3.13:

RuntimeError: __class__ not set defining 'Foo' as <class '__main__.Foo'>.
Was __classcell__ propagated to type.__new__?

In Python ≥ 3.14 (after python/cpython#130082):

TypeError: uses of super() and __class__ are unsupported in methods of NamedTuple subclasses

typing.NamedTuple is implemented as a function that dynamically creates and returns a new class (via collections.namedtuple). This does not propagate the __classcell__, so the __class__ closure variable is never bound — causing zero-argument super() to fail.

Reapplying typing.NamedTuple logic is impossible

Attempting to reapply NamedTuple processing to a subclass fails:

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 "make-typed-namedtuples-final.py", line 6, in <module>
    class Bar(Foo, metaclass=NamedTupleMeta):
        y: int
  File ".../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 typing.NamedTuple class becomes type, not typing.NamedTupleMeta. Therefore typing.NamedTupleMeta only works for typing.NamedTuple classes, but not their subclasses.

Why this matters

  • The current behavior is surprising and inconsistent with other Python data modeling tools, violating the principle of least astonishment.
  • It has been a source of confusion and bug reports for some years.
  • Preventing subclassing would make the intended use of NamedTuple explicit:
    lightweight, immutable, iterable, slot-based, final data structures.

Proposed change

at runtime

  • Python 3.15: Subclassing a NamedTuple emits a DeprecationWarning.
  • Python 3.20: Subclassing a NamedTuple raises immediately:
TypeError: typing.NamedTuple classes cannot be subclassed

in the typing specification

  • Immediately after acceptance, the typing spec requires type checkers to treat all NamedTuple classes as if decorated with @final.

Prior art and discussion

  • python/typing#427 — constructor behavior first reported by @guido (2017).
  • python/mypy#3521 — request for subtyping support was discussed and not proposed.
  • Functional API for NamedTuple already supports field composition without subclassing.

Reference implementation


What do you think about this idea? Would it be useful for Python programmers? Would it help them align with the intended design of typing.NamedTuple?

6 Likes

In scipy-stubs the following pattern is used to avoid code duplication:

@type_check_only
class _TestResultTuple(NamedTuple, Generic[_FloatOrArrayT_co]):
    statistic: _FloatOrArrayT_co
    pvalue: _FloatOrArrayT_co


class SkewtestResult(_TestResultTuple[_FloatOrArrayT_co], Generic[_FloatOrArrayT_co]): ...
class KurtosistestResult(_TestResultTuple[_FloatOrArrayT_co], Generic[_FloatOrArrayT_co]): ...
# and 8 more like these

As far as I can tell, this is a valid use-case of NamedTuple inheritance.

Since you can only inherit from a NamedTuple type and Generic, you cannot simply write

class Point2D(NamedTuple, MixIn):
    x: int
    y: int

You need to use the following trick to add a mix-in in a named tuple class:

class Point2D(NamedTuple):
    x: int
    y: int

class Point2D(Point2D, MixIn):
    pass

And now you propose to forbid this too.

2 Likes

The library reference is clear on what subclassing is and is not for:

Since a named tuple is a regular Python class, it is easy to add or change functionality with a subclass. Here is how to add a calculated field and a fixed-width print format:

Subclassing is not useful for adding new, stored fields. Instead, simply create a new named tuple type from the _fields attribute:

Point3D = namedtuple('Point3D', Point._fields + ('z',))

Subclassing is not always for the purpose of adding new fields. Marking NamedTuple subclasses as @final will prevent the use of mixins on such subclasses. The standard library urllib.parse makes use of NamedTuples and mixins extensively to add new properties that are derived from the fields contained in the base class.

In general one needs to be careful when using NamedTuples. Their inherent sequence semantics just isn’t always suitable for many cases, and they are not drop-in replacements of dataclasses or plain objects. They are often used to evolve a typed-based API in order to give the items more descriptive names, but are not necessarily designed to be further extended by users.

Thanks for these well-thought replies!

The mixins case is a good catch.

Not only mixins, but simple methods that work on the data in some particular way, possibly to implement some structural type.

I could be pushing the idea, and specify that we only raise upon seeing extra annotations. That’s somewhat similar to @runtime_checkable protocols prohibiting non-method members in issubclass() calls. But that I think is getting too convoluted and not in the “consenting adults” philosophy.

I think it’s too late to change this now and would only break code for the sake of deemed correctness.

It would be much better to clarify that subclassing typing.NamedTuple classes is an advanced use case (which it is), and you’d probably choose dataclasses.

The library reference is clear on what subclassing is and is not for:

@matthewyu0311, I see that you’re quoting the collections.namedtuple documentation here, not typing.NamedTuple’s. And it’s a good quote! Now since most people (should) use typing.NamedTuple, I’ll allow myself to propose an update to these docs as well to cover inheritance.

Do you think that that could be a good conclusion of this discussion?

3 Likes

I think it would be a good idea to make sure that the typing specification forbids new fields on NamedTuple sub-subclasses. So that at the very least type checkers can error when new fields are declared on a subclass.

1 Like