# `NamedTuple` `NamedTuple` is a type-safe way to define named tuples — a tuple where each field can be accessed by name, and not just by its numeric position within the tuple: ## `typing.NamedTuple` ### Basics ```py from typing import NamedTuple from ty_extensions import static_assert, is_subtype_of, is_assignable_to, reveal_mro class Person(NamedTuple): id: int name: str age: int | None = None alice = Person(1, "Alice", 42) alice = Person(id=1, name="Alice", age=42) bob = Person(2, "Bob") bob = Person(id=2, name="Bob") reveal_type(alice.id) # revealed: int reveal_type(alice.name) # revealed: str reveal_type(alice.age) # revealed: int | None # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(Person) static_assert(is_subtype_of(Person, tuple[int, str, int | None])) static_assert(is_subtype_of(Person, tuple[object, ...])) static_assert(not is_assignable_to(Person, tuple[int, str, int])) static_assert(not is_assignable_to(Person, tuple[int, str])) reveal_type(len(alice)) # revealed: Literal[3] reveal_type(bool(alice)) # revealed: Literal[True] reveal_type(alice[0]) # revealed: int reveal_type(alice[1]) # revealed: str reveal_type(alice[2]) # revealed: int | None # error: [index-out-of-bounds] "Index 3 is out of bounds for tuple `Person` with length 3" reveal_type(alice[3]) # revealed: Unknown reveal_type(alice[-1]) # revealed: int | None reveal_type(alice[-2]) # revealed: str reveal_type(alice[-3]) # revealed: int # error: [index-out-of-bounds] "Index -4 is out of bounds for tuple `Person` with length 3" reveal_type(alice[-4]) # revealed: Unknown reveal_type(alice[1:]) # revealed: tuple[str, int | None] reveal_type(alice[::-1]) # revealed: tuple[int | None, str, int] alice_id, alice_name, alice_age = alice reveal_type(alice_id) # revealed: int reveal_type(alice_name) # revealed: str reveal_type(alice_age) # revealed: int | None # error: [invalid-assignment] "Not enough values to unpack: Expected 4" a, b, c, d = alice # error: [invalid-assignment] "Too many values to unpack: Expected 2" a, b = alice *_, age = alice reveal_type(age) # revealed: int | None # error: [missing-argument] Person(3) # error: [too-many-positional-arguments] Person(3, "Eve", 99, "extra") # error: [invalid-argument-type] Person(id="3", name="Eve") reveal_type(Person.id) # revealed: property reveal_type(Person.name) # revealed: property reveal_type(Person.age) # revealed: property # error: [invalid-assignment] "Cannot assign to read-only property `id` on object of type `Person`" alice.id = 42 # error: [invalid-assignment] bob.age = None ``` Alternative functional syntax with a list of tuples: ```py Person2 = NamedTuple("Person", [("id", int), ("name", str)]) alice2 = Person2(1, "Alice") # error: [missing-argument] Person2(1) reveal_type(alice2.id) # revealed: int reveal_type(alice2.name) # revealed: str ``` Functional syntax with a tuple of tuples: ```py Person3 = NamedTuple("Person", (("id", int), ("name", str))) alice3 = Person3(1, "Alice") reveal_type(alice3.id) # revealed: int reveal_type(alice3.name) # revealed: str ``` Functional syntax with a tuple of lists: ```py Person4 = NamedTuple("Person", (["id", int], ["name", str])) alice4 = Person4(1, "Alice") reveal_type(alice4.id) # revealed: int reveal_type(alice4.name) # revealed: str ``` Functional syntax with a list of lists: ```py Person5 = NamedTuple("Person", [["id", int], ["name", str]]) alice5 = Person5(1, "Alice") reveal_type(alice5.id) # revealed: int reveal_type(alice5.name) # revealed: str ``` ### Functional syntax with variable name When the typename is passed via a variable, we can extract it from the inferred literal string type: ```py from typing import NamedTuple name = "Person" Person = NamedTuple(name, [("id", int), ("name", str)]) p = Person(1, "Alice") reveal_type(p.id) # revealed: int reveal_type(p.name) # revealed: str ``` ### Functional syntax with tuple variable fields When fields are passed via a tuple variable, we can extract the literal field names and types from the inferred tuple type: ```py from typing import NamedTuple from ty_extensions import static_assert, is_subtype_of, reveal_mro fields = (("host", str), ("port", int)) Url = NamedTuple("Url", fields) url = Url("localhost", 8080) reveal_type(url.host) # revealed: str reveal_type(url.port) # revealed: int # Generic types are also correctly converted to instance types. generic_fields = (("items", list[int]), ("mapping", dict[str, bool])) Container = NamedTuple("Container", generic_fields) container = Container([1, 2, 3], {"a": True}) reveal_type(container.items) # revealed: list[int] reveal_type(container.mapping) # revealed: dict[str, bool] # MRO includes the properly specialized tuple type. # revealed: (, , ) reveal_mro(Url) static_assert(is_subtype_of(Url, tuple[str, int])) # Invalid type expressions in fields produce a diagnostic. invalid_fields = (("x", 42),) # 42 is not a valid type # error: [invalid-type-form] "Invalid type `Literal[42]` in `NamedTuple` field type" InvalidNT = NamedTuple("InvalidNT", invalid_fields) reveal_type(InvalidNT) # revealed: # Unpacking works correctly with the field types. host, port = url reveal_type(host) # revealed: str reveal_type(port) # revealed: int # error: [invalid-assignment] "Too many values to unpack: Expected 1" (only_one,) = url # error: [invalid-assignment] "Not enough values to unpack: Expected 3" a, b, c = url # Indexing works correctly. reveal_type(url[0]) # revealed: str reveal_type(url[1]) # revealed: int # error: [index-out-of-bounds] url[2] ``` ### Functional syntax with variadic tuple fields When fields are passed as a variadic tuple (e.g., `tuple[..., *tuple[T, ...]]`), we cannot determine the exact field count statically. In this case, we fall back to unknown fields: ```toml [environment] python-version = "3.11" ``` ```py from typing import NamedTuple from ty_extensions import reveal_mro # Variadic tuple - we can't determine the exact fields statically. def get_fields() -> tuple[tuple[str, type[int]], *tuple[tuple[str, type[str]], ...]]: return (("x", int), ("y", str)) fields = get_fields() NT = NamedTuple("NT", fields) # Fields are unknown, so attribute access returns Any and MRO has Unknown tuple. reveal_type(NT) # revealed: reveal_mro(NT) # revealed: (, , ) reveal_type(NT(1, "a").x) # revealed: Any ``` Similarly for `collections.namedtuple`: ```py import collections from ty_extensions import reveal_mro def get_field_names() -> tuple[str, *tuple[str, ...]]: return ("x", "y") field_names = get_field_names() NT = collections.namedtuple("NT", field_names) # Fields are unknown, so attribute access returns Any and MRO has Unknown tuple. reveal_type(NT) # revealed: reveal_mro(NT) # revealed: (, , ) reveal_type(NT(1, 2).x) # revealed: Any ``` ### Class inheriting from functional NamedTuple Classes can inherit from functional namedtuples. The constructor parameters and field types are properly inherited: ```py from typing import NamedTuple from ty_extensions import reveal_mro class Url(NamedTuple("Url", [("host", str), ("path", str)])): pass reveal_type(Url) # revealed: # revealed: (, , , ) reveal_mro(Url) reveal_type(Url.__new__) # revealed: (cls: type, host: str, path: str) -> Url # Constructor works with the inherited fields. url = Url("example.com", "/path") reveal_type(url) # revealed: Url reveal_type(url.host) # revealed: str reveal_type(url.path) # revealed: str # Error handling works correctly. # error: [missing-argument] Url("example.com") # error: [too-many-positional-arguments] Url("example.com", "/path", "extra") ``` Subclasses can add methods that use inherited fields: ```py from typing import NamedTuple from typing_extensions import Self class Url(NamedTuple("Url", [("host", str), ("port", int)])): def with_port(self, port: int) -> Self: reveal_type(self.host) # revealed: str reveal_type(self.port) # revealed: int return self._replace(port=port) url = Url("localhost", 8080) reveal_type(url.with_port(9000)) # revealed: Url ``` For `class Foo(namedtuple("Foo", ...)): ...`, the inner call creates a namedtuple class, but the outer class is just a regular class inheriting from it. This is equivalent to: ```py class _Foo(NamedTuple): ... class Foo(_Foo): # Regular class, not a namedtuple ... ``` Because the outer class is not itself a namedtuple, it can use `super()` and override `__new__`: ```py from collections import namedtuple from typing import NamedTuple class ExtType(namedtuple("ExtType", "code data")): """Override __new__ to add validation.""" def __new__(cls, code, data): if not isinstance(code, int): raise TypeError("code must be int") return super().__new__(cls, code, data) class Url(NamedTuple("Url", [("host", str), ("path", str)])): """Override __new__ to normalize the path.""" def __new__(cls, host, path): if path and not path.startswith("/"): path = "/" + path return super().__new__(cls, host, path) # Both work correctly. ext = ExtType(42, b"hello") reveal_type(ext) # revealed: ExtType url = Url("example.com", "path") reveal_type(url) # revealed: Url ``` ### Functional syntax with list variable fields When fields are passed via a list variable (not a literal), the field names cannot be determined statically. Attribute access returns `Any` and the constructor accepts any arguments: ```py from typing import NamedTuple from typing_extensions import Self fields = [("host", str), ("port", int)] class Url(NamedTuple("Url", fields)): def with_port(self, port: int) -> Self: # Fields are unknown, so attribute access returns Any. reveal_type(self.host) # revealed: Any reveal_type(self.port) # revealed: Any reveal_type(self.unknown) # revealed: Any return self._replace(port=port) ``` When constructing a namedtuple directly with dynamically-defined fields, keyword arguments are accepted because the constructor uses a gradual signature: ```py import collections from ty_extensions import reveal_mro CheckerConfig = ["duration", "video_fps", "audio_sample_rate"] GroundTruth = collections.namedtuple("GroundTruth", " ".join(CheckerConfig)) # No error - fields are unknown, so any keyword arguments are accepted config = GroundTruth(duration=0, video_fps=30, audio_sample_rate=44100) reveal_type(config) # revealed: GroundTruth reveal_type(config.duration) # revealed: Any # Namedtuples with unknown fields inherit from tuple[Unknown, ...] to avoid false positives. # revealed: (, , ) reveal_mro(GroundTruth) # No index-out-of-bounds error since the tuple length is unknown. reveal_type(config[0]) # revealed: Unknown reveal_type(config[100]) # revealed: Unknown ``` ### Functional syntax signature validation The `collections.namedtuple` function accepts `str | Iterable[str]` for `field_names`: ```py import collections from ty_extensions import reveal_mro # String field names (space-separated) Point1 = collections.namedtuple("Point", "x y") reveal_type(Point1) # revealed: reveal_mro(Point1) # revealed: (, , ) # String field names with multiple spaces Point1a = collections.namedtuple("Point", "x y") reveal_type(Point1a) # revealed: reveal_mro(Point1a) # revealed: (, , ) # String field names (comma-separated also works at runtime) Point2 = collections.namedtuple("Point", "x, y") reveal_type(Point2) # revealed: reveal_mro(Point2) # revealed: (, , ) # List of strings Point3 = collections.namedtuple("Point", ["x", "y"]) reveal_type(Point3) # revealed: reveal_mro(Point3) # revealed: (, , ) # Tuple of strings Point4 = collections.namedtuple("Point", ("x", "y")) reveal_type(Point4) # revealed: reveal_mro(Point4) # revealed: (, , ) # Invalid: integer is not a valid typename # error: [invalid-argument-type] Invalid = collections.namedtuple(123, ["x", "y"]) reveal_type(Invalid) # revealed: '> reveal_mro(Invalid) # revealed: ('>, , ) # Invalid: too many positional arguments # error: [too-many-positional-arguments] "Too many positional arguments to function `namedtuple`: expected 2, got 4" TooMany = collections.namedtuple("TooMany", "x", "y", "z") reveal_type(TooMany) # revealed: ``` The `typing.NamedTuple` function accepts `Iterable[tuple[str, Any]]` for `fields`: ```py from typing import NamedTuple # List of tuples Person1 = NamedTuple("Person", [("name", str), ("age", int)]) reveal_type(Person1) # revealed: # Tuple of tuples Person2 = NamedTuple("Person", (("name", str), ("age", int))) reveal_type(Person2) # revealed: # Invalid: integer is not a valid typename # error: [invalid-argument-type] NamedTuple(123, [("name", str)]) # Invalid: too many positional arguments # error: [too-many-positional-arguments] "Too many positional arguments to function `NamedTuple`: expected 2, got 4" TooMany = NamedTuple("TooMany", [("x", int)], "extra", "args") reveal_type(TooMany) # revealed: ``` ### Keyword arguments for `collections.namedtuple` The `collections.namedtuple` function accepts `rename`, `defaults`, and `module` keyword arguments: ```py import collections from ty_extensions import reveal_mro # `rename=True` replaces invalid identifiers with positional names Point = collections.namedtuple("Point", ["x", "class", "_y", "z", "z"], rename=True) reveal_type(Point) # revealed: reveal_type(Point.__new__) # revealed: (cls: type, x: Any, _1: Any, _2: Any, z: Any, _4: Any) -> Point reveal_mro(Point) # revealed: (, , ) p = Point(1, 2, 3, 4, 5) reveal_type(p.x) # revealed: Any reveal_type(p._1) # revealed: Any reveal_type(p._2) # revealed: Any reveal_type(p.z) # revealed: Any reveal_type(p._4) # revealed: Any # Truthy non-bool values for `rename` are also handled, but emit a diagnostic # error: [invalid-argument-type] "Invalid argument to parameter `rename` of `namedtuple()`" Point2 = collections.namedtuple("Point2", ["_x", "class"], rename=1) reveal_type(Point2) # revealed: reveal_type(Point2.__new__) # revealed: (cls: type, _0: Any, _1: Any) -> Point2 # `defaults` provides default values for the rightmost fields Person = collections.namedtuple("Person", ["name", "age", "city"], defaults=["Unknown"]) reveal_type(Person) # revealed: reveal_type(Person.__new__) # revealed: (cls: type, name: Any, age: Any, city: Any = "Unknown") -> Person reveal_mro(Person) # revealed: (, , ) # Can create with all fields person1 = Person("Alice", 30, "NYC") # Can omit the field with default person2 = Person("Bob", 25) reveal_type(person1.city) # revealed: Any reveal_type(person2.city) # revealed: Any # `module` is valid but doesn't affect type checking Config = collections.namedtuple("Config", ["host", "port"], module="myapp") reveal_type(Config) # revealed: # When more defaults are provided than fields, we treat all fields as having defaults. # TODO: This should emit a diagnostic since it would fail at runtime. TooManyDefaults = collections.namedtuple("TooManyDefaults", ["x", "y"], defaults=("a", "b", "c")) reveal_type(TooManyDefaults) # revealed: reveal_type(TooManyDefaults.__new__) # revealed: (cls: type, x: Any = "a", y: Any = "b") -> TooManyDefaults # Unknown keyword arguments produce an error # error: [unknown-argument] Bad1 = collections.namedtuple("Bad1", ["x", "y"], foobarbaz=42) reveal_type(Bad1) # revealed: reveal_mro(Bad1) # revealed: (, , ) # Multiple unknown keyword arguments # error: [unknown-argument] # error: [unknown-argument] Bad2 = collections.namedtuple("Bad2", ["x"], invalid1=True, invalid2=False) reveal_type(Bad2) # revealed: reveal_mro(Bad2) # revealed: (, , ) # Invalid type for `defaults` (not Iterable[Any] | None) # error: [invalid-argument-type] "Invalid argument to parameter `defaults` of `namedtuple()`" Bad3 = collections.namedtuple("Bad3", ["x"], defaults=123) reveal_type(Bad3) # revealed: # Invalid type for `module` (not str | None) # error: [invalid-argument-type] "Invalid argument to parameter `module` of `namedtuple()`" Bad4 = collections.namedtuple("Bad4", ["x"], module=456) reveal_type(Bad4) # revealed: # Invalid type for `field_names` (not str | Iterable[str]) # error: [invalid-argument-type] "Invalid argument to parameter `field_names` of `namedtuple()`" Bad5 = collections.namedtuple("Bad5", 12345) reveal_type(Bad5) # revealed: ``` ### Keyword arguments for `typing.NamedTuple` The `typing.NamedTuple` function does not accept any keyword arguments: ```py from typing import NamedTuple # error: [unknown-argument] Bad3 = NamedTuple("Bad3", [("x", int)], rename=True) # error: [unknown-argument] Bad4 = NamedTuple("Bad4", [("x", int)], defaults=[0]) # error: [unknown-argument] Bad5 = NamedTuple("Bad5", [("x", int)], foobarbaz=42) # Invalid type for `fields` (not an iterable) # error: [invalid-argument-type] "Invalid argument to parameter `fields` of `NamedTuple()`" Bad6 = NamedTuple("Bad6", 12345) reveal_type(Bad6) # revealed: ``` ### Starred and double-starred arguments When starred (`*args`) or double-starred (`**kwargs`) arguments are used, we fall back to normal call binding since we can't statically determine the arguments. This results in `NamedTupleFallback` being returned: ```py import collections from typing import NamedTuple args = ("Point", ["x", "y"]) kwargs = {"rename": True} # Starred positional arguments - falls back to NamedTupleFallback Point1 = collections.namedtuple(*args) reveal_type(Point1) # revealed: type[NamedTupleFallback] # Ideally we'd catch this false negative, # but it's unclear if it's worth the added complexity Point2 = NamedTuple(*args) reveal_type(Point2) # revealed: type[NamedTupleFallback] # Double-starred keyword arguments - falls back to NamedTupleFallback Point3 = collections.namedtuple("Point", ["x", "y"], **kwargs) reveal_type(Point3) # revealed: type[NamedTupleFallback] Point4 = NamedTuple("Point", [("x", int), ("y", int)], **kwargs) reveal_type(Point4) # revealed: type[NamedTupleFallback] ``` ### Definition Fields without default values must come before fields with. ```py from typing import NamedTuple class Location(NamedTuple): altitude: float = 0.0 # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitude` defined here without a default value" latitude: float # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitude` defined here without a default value" longitude: float class StrangeLocation(NamedTuple): altitude: float altitude: float = 0.0 altitude: float altitude: float = 0.0 latitude: float # error: [invalid-named-tuple] longitude: float # error: [invalid-named-tuple] class VeryStrangeLocation(NamedTuple): altitude: float = 0.0 latitude: float # error: [invalid-named-tuple] longitude: float # error: [invalid-named-tuple] altitude: float = 0.0 ``` ### Multiple Inheritance Multiple inheritance is not supported for `NamedTuple` classes except with `Generic`: ```py from typing import NamedTuple, Protocol # error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`" class C(NamedTuple, object): id: int # fmt: off class D( int, # error: [invalid-named-tuple] NamedTuple ): ... # fmt: on # error: [invalid-named-tuple] class E(NamedTuple, Protocol): ... ``` However, as explained above, for `class Foo(namedtuple("Foo", ...)): ...` the outer class is not itself a namedtuple—it just inherits from one. So it can use multiple inheritance freely: ```py from abc import ABC from collections import namedtuple from typing import NamedTuple class Point(namedtuple("Point", ["x", "y"]), ABC): """No error - functional namedtuple inheritance allows multiple inheritance.""" class Url(NamedTuple("Url", [("host", str), ("port", int)]), ABC): """No error - typing.NamedTuple functional syntax also allows multiple inheritance.""" p = Point(1, 2) reveal_type(p.x) # revealed: Any reveal_type(p.y) # revealed: Any u = Url("localhost", 8080) reveal_type(u.host) # revealed: str reveal_type(u.port) # revealed: int ``` ### Inherited tuple methods Namedtuples inherit methods from their tuple base class, including `count`, `index`, and comparison methods (`__lt__`, `__le__`, `__gt__`, `__ge__`). ```py from collections import namedtuple from typing import NamedTuple # typing.NamedTuple inherits tuple methods class Point(NamedTuple): x: int y: int p = Point(1, 2) reveal_type(p.count(1)) # revealed: int reveal_type(p.index(2)) # revealed: int # collections.namedtuple also inherits tuple methods Person = namedtuple("Person", ["name", "age"]) alice = Person("Alice", 30) reveal_type(alice.count("Alice")) # revealed: int reveal_type(alice.index(30)) # revealed: int ``` The `@total_ordering` decorator should not emit a diagnostic, since the required `__lt__` method is already present: ```py from collections import namedtuple from functools import total_ordering from typing import NamedTuple # No error - __lt__ is inherited from the tuple base class @total_ordering class Point(namedtuple("Point", "x y")): ... p1 = Point(1, 2) p2 = Point(3, 4) # TODO: should be `bool`, not `Any | Literal[False]` reveal_type(p1 < p2) # revealed: Any | Literal[False] reveal_type(p1 <= p2) # revealed: Any | Literal[True] # Same for typing.NamedTuple - no error @total_ordering class Person(NamedTuple): name: str age: int alice = Person("Alice", 30) bob = Person("Bob", 25) reveal_type(alice < bob) # revealed: bool reveal_type(alice >= bob) # revealed: bool ``` ### Inheriting from a `NamedTuple` Inheriting from a `NamedTuple` is supported, but new fields on the subclass will not be part of the synthesized `__new__` signature: ```py from typing import NamedTuple class User(NamedTuple): id: int name: str class SuperUser(User): level: int # This is fine: alice = SuperUser(1, "Alice") reveal_type(alice.level) # revealed: int # This is an error because `level` is not part of the signature: # error: [too-many-positional-arguments] alice = SuperUser(1, "Alice", 3) ``` TODO: If any fields added by the subclass conflict with those in the base class, that should be flagged. ```py from typing import NamedTuple class User(NamedTuple): id: int name: str age: int | None nickname: str class SuperUser(User): # TODO: this should be an error because it implies that the `id` attribute on # `SuperUser` is mutable, but the read-only `id` property from the superclass # has not been overridden in the class body id: int # this is fine; overriding a read-only attribute with a mutable one # does not conflict with the Liskov Substitution Principle name: str = "foo" # this is also fine @property def age(self) -> int: return super().age or 42 def now_called_robert(self): self.name = "Robert" # fine because overridden with a mutable attribute # error: 9 [invalid-assignment] "Cannot assign to read-only property `nickname` on object of type `Self@now_called_robert`" self.nickname = "Bob" james = SuperUser(0, "James", 42, "Jimmy") # fine because the property on the superclass was overridden with a mutable attribute # on the subclass james.name = "Robert" # error: [invalid-assignment] "Cannot assign to read-only property `nickname` on object of type `SuperUser`" james.nickname = "Bob" ``` ### Generic named tuples ```toml [environment] python-version = "3.12" ``` ```py from typing import NamedTuple, Generic, TypeVar class Property[T](NamedTuple): name: str value: T reveal_type(Property("height", 3.4)) # revealed: Property[float] reveal_type(Property.value) # revealed: property reveal_type(Property.value.fget) # revealed: (self, /) -> Unknown reveal_type(Property[str].value.fget) # revealed: (self, /) -> str reveal_type(Property("height", 3.4).value) # revealed: float T = TypeVar("T") class LegacyProperty(NamedTuple, Generic[T]): name: str value: T reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int] reveal_type(LegacyProperty.value) # revealed: property reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str reveal_type(LegacyProperty("height", 3.4).value) # revealed: int | float ``` Generic namedtuples can also be defined using the functional syntax with type variables in the field types. We don't currently support this, but mypy does: ```py from typing import NamedTuple, TypeVar T = TypeVar("T") # TODO: ideally this would create a generic namedtuple class Pair = NamedTuple("Pair", [("first", T), ("second", T)]) # For now, the TypeVar is not specialized, so the field types remain as `T@Pair` and argument type # errors are emitted when calling the constructor. reveal_type(Pair) # revealed: # error: [invalid-argument-type] # error: [invalid-argument-type] reveal_type(Pair(1, 2)) # revealed: Pair # error: [invalid-argument-type] # error: [invalid-argument-type] reveal_type(Pair(1, 2).first) # revealed: T@Pair # error: [invalid-argument-type] # error: [invalid-argument-type] reveal_type(Pair(1, 2).second) # revealed: T@Pair ``` ## Attributes on `NamedTuple` The following attributes are available on `NamedTuple` classes / instances: ```py from typing import NamedTuple class Person(NamedTuple): name: str age: int | None = None reveal_type(Person._field_defaults) # revealed: dict[str, Any] reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]] reveal_type(Person.__slots__) # revealed: tuple[()] reveal_type(Person._make) # revealed: bound method ._make(iterable: Iterable[Any]) -> Person reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any] reveal_type(Person._replace) # revealed: (self: Self, *, name: str = ..., age: int | None = ...) -> Self reveal_type(Person._make(("Alice", 42))) # revealed: Person person = Person("Alice", 42) reveal_type(person._asdict()) # revealed: dict[str, Any] reveal_type(person._replace(name="Bob")) # revealed: Person # Invalid keyword arguments are detected: # error: [unknown-argument] "Argument `invalid` does not match any known parameter" person._replace(invalid=42) ``` When accessing them on child classes of generic `NamedTuple`s, the return type is specialized accordingly: ```py from typing import NamedTuple, Generic, TypeVar T = TypeVar("T") class Box(NamedTuple, Generic[T]): content: T class IntBox(Box[int]): pass reveal_type(IntBox(1)._replace(content=42)) # revealed: IntBox ``` ## `collections.namedtuple` ```py from collections import namedtuple Person = namedtuple("Person", ["id", "name", "age"], defaults=[None]) alice = Person(1, "Alice", 42) bob = Person(2, "Bob") reveal_type(Person.__slots__) # revealed: tuple[()] ``` ## `collections.namedtuple` with tuple variable field names When field names are passed via a tuple variable, we can extract the literal field names from the inferred tuple type. The class is properly synthesized (not a fallback), but field types are `Any` since `collections.namedtuple` doesn't include type annotations: ```py from collections import namedtuple field_names = ("name", "age") Person = namedtuple("Person", field_names) reveal_type(Person) # revealed: alice = Person("Alice", 42) reveal_type(alice) # revealed: Person reveal_type(alice.name) # revealed: Any reveal_type(alice.age) # revealed: Any ``` ## `collections.namedtuple` with list variable field names When field names are passed via a list variable (not a literal), we fall back to `NamedTupleFallback` which allows any attribute access. This is a regression test for accessing `Self` attributes in methods of classes that inherit from namedtuples with dynamic fields: ```py from collections import namedtuple from typing_extensions import Self field_names = ["host", "port"] class Url(namedtuple("Url", field_names)): def with_port(self, port: int) -> Self: # Fields are unknown, so attribute access returns `Any`. reveal_type(self.host) # revealed: Any reveal_type(self.port) # revealed: Any reveal_type(self.unknown) # revealed: Any return self._replace(port=port) ``` ## `collections.namedtuple` attributes Functional namedtuples have synthesized attributes similar to class-based namedtuples: ```py from collections import namedtuple Person = namedtuple("Person", ["name", "age"]) reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]] reveal_type(Person._field_defaults) # revealed: dict[str, Any] reveal_type(Person._make) # revealed: bound method ._make(iterable: Iterable[Any]) -> Person reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any] reveal_type(Person._replace) # revealed: (self: Self, *, name: Any = ..., age: Any = ...) -> Self # _make creates instances from an iterable. reveal_type(Person._make(["Alice", 30])) # revealed: Person # _asdict converts to a dictionary. person = Person("Alice", 30) reveal_type(person._asdict()) # revealed: dict[str, Any] # _replace creates a copy with replaced fields. reveal_type(person._replace(name="Bob")) # revealed: Person ``` ## The symbol `NamedTuple` itself At runtime, `NamedTuple` is a function, and we understand this: ```py import types import typing def expects_functiontype(x: types.FunctionType): ... expects_functiontype(typing.NamedTuple) ``` This means we also understand that all attributes on function objects are available on the symbol `typing.NamedTuple`: ```py reveal_type(typing.NamedTuple.__name__) # revealed: str reveal_type(typing.NamedTuple.__qualname__) # revealed: str reveal_type(typing.NamedTuple.__kwdefaults__) # revealed: dict[str, Any] | None # error: [unresolved-attribute] reveal_type(typing.NamedTuple.__mro__) # revealed: Unknown ``` By the normal rules, `NamedTuple` and `type[NamedTuple]` should not be valid in type expressions -- there is no object at runtime that is an "instance of `NamedTuple`", nor is there any class at runtime that is a "subclass of `NamedTuple`" -- these are both impossible, since `NamedTuple` is a function and not a class. However, for compatibility with other type checkers, we allow `NamedTuple` in type expressions and understand it as describing an interface that all `NamedTuple` classes would satisfy: ```py def expects_named_tuple(x: typing.NamedTuple): reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(...) -> NamedTupleLike # revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], [_T](value: tuple[_T, ...], /) -> tuple[object, ...]] reveal_type(x.__add__) reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object] def _(y: type[typing.NamedTuple]): reveal_type(y) # revealed: @Todo(unsupported type[X] special form) # error: [invalid-type-form] "Special form `typing.NamedTuple` expected no type parameter" def _(z: typing.NamedTuple[int]): ... ``` NamedTuples are assignable to `NamedTupleLike`. The `NamedTupleLike._replace` method is typed with `(*args, **kwargs)`, which type checkers treat as equivalent to `...` (per the typing spec), making all NamedTuple implementations automatically compatible: ```py from typing import NamedTuple, Protocol, Iterable, Any from ty_extensions import static_assert, is_assignable_to class Point(NamedTuple): x: int y: int reveal_type(Point._make) # revealed: bound method ._make(iterable: Iterable[Any]) -> Point reveal_type(Point._asdict) # revealed: def _asdict(self) -> dict[str, Any] reveal_type(Point._replace) # revealed: (self: Self, *, x: int = ..., y: int = ...) -> Self # Point is assignable to NamedTuple. static_assert(is_assignable_to(Point, NamedTuple)) # NamedTuple instances can be passed to functions expecting NamedTupleLike. expects_named_tuple(Point(x=42, y=56)) # But plain tuples are not NamedTupleLike (they don't have _make, _asdict, _replace, etc.). # error: [invalid-argument-type] "Argument to function `expects_named_tuple` is incorrect: Expected `tuple[object, ...] & NamedTupleLike`, found `tuple[Literal[1], Literal[2]]`" expects_named_tuple((1, 2)) ``` The type described by `NamedTuple` in type expressions is understood as being assignable to `tuple[object, ...]` and `tuple[Any, ...]`: ```py static_assert(is_assignable_to(NamedTuple, tuple)) static_assert(is_assignable_to(NamedTuple, tuple[object, ...])) static_assert(is_assignable_to(NamedTuple, tuple[Any, ...])) def expects_tuple(x: tuple[object, ...]): ... def _(x: NamedTuple): expects_tuple(x) # fine ``` ## NamedTuple with custom `__getattr__` This is a regression test for . Make sure that the `__getattr__` method does not interfere with the `NamedTuple` behavior. ```py from typing import NamedTuple class Vec2(NamedTuple): x: float = 0.0 y: float = 0.0 def __getattr__(self, attrs: str): ... Vec2(0.0, 0.0) ``` ## `super()` is not supported in NamedTuple methods Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime. In Python 3.14+, a `TypeError` is raised; in earlier versions, a confusing `RuntimeError` about `__classcell__` is raised. ```py from typing import NamedTuple class F(NamedTuple): x: int def method(self): # error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`" super() def method_with_args(self): # error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`" super(F, self) def method_with_different_pivot(self): # Even passing a different pivot class fails. # error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`" super(tuple, self) @classmethod def class_method(cls): # error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`" super() @staticmethod def static_method(): # error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`" super() @property def prop(self): # error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`" return super() ``` However, classes that **inherit from** a `NamedTuple` class (but don't directly inherit from `NamedTuple`) can use `super()` normally: ```py from typing import NamedTuple class Base(NamedTuple): x: int class Child(Base): def method(self): super() ``` And regular classes that don't inherit from `NamedTuple` at all can use `super()` as normal: ```py class Regular: def method(self): super() # fine ``` Using `super()` on a `NamedTuple` class also works fine if it occurs outside the class: ```py from typing import NamedTuple class F(NamedTuple): x: int super(F, F(42)) # fine ``` ## NamedTuples cannot have field names starting with underscores ```py from typing import NamedTuple class Foo(NamedTuple): # error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore" _bar: int class Bar(NamedTuple): x: int class Baz(Bar): _whatever: str # `Baz` is not a NamedTuple class, so this is fine ``` ## Prohibited NamedTuple attributes `NamedTuple` classes have certain synthesized attributes that cannot be overwritten. Attempting to assign to these attributes (without type annotations) will raise an `AttributeError` at runtime. ```py from typing import NamedTuple class F(NamedTuple): x: int # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`" _asdict = 42 # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_make`" _make = "foo" # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_replace`" _replace = lambda self: self # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_fields`" _fields = () # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_field_defaults`" _field_defaults = {} # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__new__`" __new__ = None # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__init__`" __init__ = None # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__getnewargs__`" __getnewargs__ = None ``` However, other attributes (including those starting with underscores) can be assigned without error: ```py from typing import NamedTuple class G(NamedTuple): x: int # These are fine (not prohibited attributes) _custom = 42 __custom__ = "ok" regular_attr = "value" ``` Note that type-annotated attributes become NamedTuple fields, not attribute overrides. They are not flagged as prohibited attribute overrides (though field names starting with `_` are caught by the underscore field name check): ```py from typing import NamedTuple class H(NamedTuple): x: int # This is a field declaration, not an override. It's not flagged as an override, # but is flagged because field names cannot start with underscores. # error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore" _asdict: int = 0 ``` The check also applies to assignments within conditional blocks: ```py from typing import NamedTuple class I(NamedTuple): x: int if True: # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`" _asdict = 42 ``` Method definitions with prohibited names are also flagged: ```py from typing import NamedTuple class J(NamedTuple): x: int # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`" def _asdict(self): return {} @classmethod # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_make`" def _make(cls, iterable): return cls(*iterable) ``` Classes that inherit from a `NamedTuple` class (but don't directly inherit from `NamedTuple`) are not subject to these restrictions: ```py from typing import NamedTuple class Base(NamedTuple): x: int class Child(Base): # This is fine - Child is not directly a NamedTuple _asdict = 42 ``` ## Edge case: multiple reachable definitions with distinct issues ```py from typing import NamedTuple def coinflip() -> bool: return True class Foo(NamedTuple): if coinflip(): _asdict: bool # error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore" else: # TODO: there should only be one diagnostic here... # # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`" # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`" _asdict = True ```