Skip to content

Commit 03d8679

Browse files
sharkdpdcreager
andauthored
[red-knot] Preliminary NamedTuple support (#17738)
## Summary Adds preliminary support for `NamedTuple`s, including: * No false positives when constructing a `NamedTuple` object * Correct signature for the synthesized `__new__` method, i.e. proper checking of constructor calls * A patched MRO (`NamedTuple` => `tuple`), mainly to make type inference of named attributes possible, but also to better reflect the runtime MRO. All of this works: ```py from typing import NamedTuple class Person(NamedTuple): id: int name: str age: int | None = None alice = Person(1, "Alice", 42) alice = Person(id=1, name="Alice", age=42) reveal_type(alice.id) # revealed: int reveal_type(alice.name) # revealed: str reveal_type(alice.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") ``` Not included: * type inference for index-based access. * support for the functional `MyTuple = NamedTuple("MyTuple", […])` syntax ## Test Plan New Markdown tests ## Ecosystem analysis ``` Diagnostic Analysis Report ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┓ ┃ Diagnostic ID ┃ Severity ┃ Removed ┃ Added ┃ Net Change ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━┩ │ lint:call-non-callable │ error │ 0 │ 3 │ +3 │ │ lint:call-possibly-unbound-method │ warning │ 0 │ 4 │ +4 │ │ lint:invalid-argument-type │ error │ 0 │ 72 │ +72 │ │ lint:invalid-context-manager │ error │ 0 │ 2 │ +2 │ │ lint:invalid-return-type │ error │ 0 │ 2 │ +2 │ │ lint:missing-argument │ error │ 0 │ 46 │ +46 │ │ lint:no-matching-overload │ error │ 19121 │ 0 │ -19121 │ │ lint:not-iterable │ error │ 0 │ 6 │ +6 │ │ lint:possibly-unbound-attribute │ warning │ 13 │ 32 │ +19 │ │ lint:redundant-cast │ warning │ 0 │ 1 │ +1 │ │ lint:unresolved-attribute │ error │ 0 │ 10 │ +10 │ │ lint:unsupported-operator │ error │ 3 │ 9 │ +6 │ │ lint:unused-ignore-comment │ warning │ 15 │ 4 │ -11 │ ├───────────────────────────────────┼──────────┼─────────┼───────┼────────────┤ │ TOTAL │ │ 19152 │ 191 │ -18961 │ └───────────────────────────────────┴──────────┴─────────┴───────┴────────────┘ Analysis complete. Found 13 unique diagnostic IDs. Total diagnostics removed: 19152 Total diagnostics added: 191 Net change: -18961 ``` I uploaded the ecosystem full diff (ignoring the 19k `no-matching-overload` diagnostics) [here](https://shark.fish/diff-namedtuple.html). * There are some new `missing-argument` false positives which come from the fact that named tuples are often created using unpacking as in `MyNamedTuple(*fields)`, which we do not understand yet. * There are some new `unresolved-attribute` false positives, because methods like `_replace` are not available. * Lots of the `invalid-argument-type` diagnostics look like true positives --------- Co-authored-by: Douglas Creager <[email protected]>
1 parent d33a503 commit 03d8679

File tree

4 files changed

+275
-115
lines changed

4 files changed

+275
-115
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# `NamedTuple`
2+
3+
`NamedTuple` is a type-safe way to define named tuples — a tuple where each field can be accessed by
4+
name, and not just by its numeric position within the tuple:
5+
6+
## `typing.NamedTuple`
7+
8+
### Basics
9+
10+
```py
11+
from typing import NamedTuple
12+
13+
class Person(NamedTuple):
14+
id: int
15+
name: str
16+
age: int | None = None
17+
18+
alice = Person(1, "Alice", 42)
19+
alice = Person(id=1, name="Alice", age=42)
20+
bob = Person(2, "Bob")
21+
bob = Person(id=2, name="Bob")
22+
23+
reveal_type(alice.id) # revealed: int
24+
reveal_type(alice.name) # revealed: str
25+
reveal_type(alice.age) # revealed: int | None
26+
27+
# TODO: These should reveal the types of the fields
28+
reveal_type(alice[0]) # revealed: Unknown
29+
reveal_type(alice[1]) # revealed: Unknown
30+
reveal_type(alice[2]) # revealed: Unknown
31+
32+
# error: [missing-argument]
33+
Person(3)
34+
35+
# error: [too-many-positional-arguments]
36+
Person(3, "Eve", 99, "extra")
37+
38+
# error: [invalid-argument-type]
39+
Person(id="3", name="Eve")
40+
```
41+
42+
Alternative functional syntax:
43+
44+
```py
45+
Person2 = NamedTuple("Person", [("id", int), ("name", str)])
46+
alice2 = Person2(1, "Alice")
47+
48+
# TODO: should be an error
49+
Person2(1)
50+
51+
reveal_type(alice2.id) # revealed: @Todo(GenericAlias instance)
52+
reveal_type(alice2.name) # revealed: @Todo(GenericAlias instance)
53+
```
54+
55+
### Multiple Inheritance
56+
57+
Multiple inheritance is not supported for `NamedTuple` classes:
58+
59+
```py
60+
from typing import NamedTuple
61+
62+
# This should ideally emit a diagnostic
63+
class C(NamedTuple, object):
64+
id: int
65+
name: str
66+
```
67+
68+
### Inheriting from a `NamedTuple`
69+
70+
Inheriting from a `NamedTuple` is supported, but new fields on the subclass will not be part of the
71+
synthesized `__new__` signature:
72+
73+
```py
74+
from typing import NamedTuple
75+
76+
class User(NamedTuple):
77+
id: int
78+
name: str
79+
80+
class SuperUser(User):
81+
level: int
82+
83+
# This is fine:
84+
alice = SuperUser(1, "Alice")
85+
reveal_type(alice.level) # revealed: int
86+
87+
# This is an error because `level` is not part of the signature:
88+
# error: [too-many-positional-arguments]
89+
alice = SuperUser(1, "Alice", 3)
90+
```
91+
92+
### Generic named tuples
93+
94+
```toml
95+
[environment]
96+
python-version = "3.12"
97+
```
98+
99+
```py
100+
from typing import NamedTuple
101+
102+
class Property[T](NamedTuple):
103+
name: str
104+
value: T
105+
106+
# TODO: this should be supported (no error, revealed type of `Property[float]`)
107+
# error: [invalid-argument-type]
108+
reveal_type(Property("height", 3.4)) # revealed: Property[Unknown]
109+
```
110+
111+
## `collections.namedtuple`
112+
113+
```py
114+
from collections import namedtuple
115+
116+
Person = namedtuple("Person", ["id", "name", "age"], defaults=[None])
117+
118+
alice = Person(1, "Alice", 42)
119+
bob = Person(2, "Bob")
120+
```

0 commit comments

Comments
 (0)