Skip to content

Commit 71c14c9

Browse files
committed
dict is not assignable to TypedDict
1 parent 770b4d1 commit 71c14c9

File tree

8 files changed

+149
-67
lines changed

8 files changed

+149
-67
lines changed

crates/ty_python_semantic/resources/mdtest/bidirectional.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def _() -> TD:
7676

7777
def _() -> TD:
7878
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
79+
# error: [invalid-return-type]
7980
return {}
8081
```
8182

crates/ty_python_semantic/resources/mdtest/call/overloads.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,8 +1685,7 @@ def int_or_str() -> int | str:
16851685
x = f([{"x": 1}], int_or_str())
16861686
reveal_type(x) # revealed: int | str
16871687

1688-
# TODO: error: [no-matching-overload] "No overload of function `f` matches arguments"
1689-
# we currently incorrectly consider `list[dict[str, int]]` a subtype of `list[T]`
1688+
# error: [no-matching-overload] "No overload of function `f` matches arguments"
16901689
f([{"y": 1}], int_or_str())
16911690
```
16921691

crates/ty_python_semantic/resources/mdtest/call/union.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,6 @@ def _(flag: bool):
277277
x = f({"x": 1})
278278
reveal_type(x) # revealed: int
279279

280-
# TODO: error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int]`"
281-
# we currently consider `TypedDict` instances to be subtypes of `dict`
280+
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[Unknown | str, Unknown | int]`"
282281
f({"y": 1})
283282
```

crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,13 @@ The type context is propagated down into the comprehension:
162162
class Person(TypedDict):
163163
name: str
164164

165+
# TODO: This should not error.
166+
# error: [invalid-assignment]
165167
persons: list[Person] = [{"name": n} for n in ["Alice", "Bob"]]
166168
reveal_type(persons) # revealed: list[Person]
167169

168-
# TODO: This should be an error
170+
# TODO: This should be an invalid-key error.
171+
# error: [invalid-assignment]
169172
invalid: list[Person] = [{"misspelled": n} for n in ["Alice", "Bob"]]
170173
```
171174

crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,20 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
3939
25 | person[str_key] = "Alice" # error: [invalid-key]
4040
26 |
4141
27 | def create_with_invalid_string_key():
42-
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
43-
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
44-
30 | from typing_extensions import ReadOnly
42+
28 | # error: [invalid-key]
43+
29 | # error: [invalid-assignment]
44+
30 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
4545
31 |
46-
32 | class Employee(TypedDict):
47-
33 | id: ReadOnly[int]
48-
34 | name: str
46+
32 | # error: [invalid-key]
47+
33 | bob = Person(name="Bob", age=25, unknown="Bar")
48+
34 | from typing_extensions import ReadOnly
4949
35 |
50-
36 | def write_to_readonly_key(employee: Employee):
51-
37 | employee["id"] = 42 # error: [invalid-assignment]
50+
36 | class Employee(TypedDict):
51+
37 | id: ReadOnly[int]
52+
38 | name: str
53+
39 |
54+
40 | def write_to_readonly_key(employee: Employee):
55+
41 | employee["id"] = 42 # error: [invalid-assignment]
5256
```
5357

5458
# Diagnostics
@@ -156,54 +160,69 @@ info: rule `invalid-key` is enabled by default
156160
157161
```
158162

163+
```
164+
error[invalid-assignment]: Object of type `dict[Unknown | str, Unknown | str | int]` is not assignable to `Person`
165+
--> src/mdtest_snippet.py:30:5
166+
|
167+
28 | # error: [invalid-key]
168+
29 | # error: [invalid-assignment]
169+
30 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
170+
| ^^^^^
171+
31 |
172+
32 | # error: [invalid-key]
173+
|
174+
info: rule `invalid-assignment` is enabled by default
175+
176+
```
177+
159178
```
160179
error[invalid-key]: Invalid key for TypedDict `Person`
161-
--> src/mdtest_snippet.py:28:21
180+
--> src/mdtest_snippet.py:30:21
162181
|
163-
27 | def create_with_invalid_string_key():
164-
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
182+
28 | # error: [invalid-key]
183+
29 | # error: [invalid-assignment]
184+
30 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
165185
| -----------------------------^^^^^^^^^--------
166186
| | |
167187
| | Unknown key "unknown"
168188
| TypedDict `Person`
169-
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
170-
30 | from typing_extensions import ReadOnly
189+
31 |
190+
32 | # error: [invalid-key]
171191
|
172192
info: rule `invalid-key` is enabled by default
173193
174194
```
175195

176196
```
177197
error[invalid-key]: Invalid key for TypedDict `Person`
178-
--> src/mdtest_snippet.py:29:11
198+
--> src/mdtest_snippet.py:33:11
179199
|
180-
27 | def create_with_invalid_string_key():
181-
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
182-
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
200+
32 | # error: [invalid-key]
201+
33 | bob = Person(name="Bob", age=25, unknown="Bar")
183202
| ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown"
184-
30 | from typing_extensions import ReadOnly
203+
34 | from typing_extensions import ReadOnly
185204
|
186205
info: rule `invalid-key` is enabled by default
187206
188207
```
189208

190209
```
191210
error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee`
192-
--> src/mdtest_snippet.py:37:5
211+
--> src/mdtest_snippet.py:41:5
193212
|
194-
36 | def write_to_readonly_key(employee: Employee):
195-
37 | employee["id"] = 42 # error: [invalid-assignment]
213+
40 | def write_to_readonly_key(employee: Employee):
214+
41 | employee["id"] = 42 # error: [invalid-assignment]
196215
| -------- ^^^^ key is marked read-only
197216
| |
198217
| TypedDict `Employee`
199218
|
200219
info: Item declaration
201-
--> src/mdtest_snippet.py:33:5
220+
--> src/mdtest_snippet.py:37:5
202221
|
203-
32 | class Employee(TypedDict):
204-
33 | id: ReadOnly[int]
222+
36 | class Employee(TypedDict):
223+
37 | id: ReadOnly[int]
205224
| ----------------- Read-only item declared here
206-
34 | name: str
225+
38 | name: str
207226
|
208227
info: rule `invalid-assignment` is enabled by default
209228

crates/ty_python_semantic/resources/mdtest/typed_dict.md

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ CAPITALIZED_NAME = "Name"
8383

8484
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
8585
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
86+
# error: [invalid-assignment]
8687
dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20}
8788

8889
def age() -> Literal["age"] | None:
@@ -95,30 +96,33 @@ The construction of a `TypedDict` is checked for type correctness:
9596

9697
```py
9798
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
99+
# error: [invalid-assignment]
98100
eve1a: Person = {"name": b"Eve", "age": None}
101+
99102
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
100103
eve1b = Person(name=b"Eve", age=None)
101104

102-
# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
103-
reveal_type(eve1a) # revealed: dict[Unknown | str, Unknown | bytes | None]
105+
reveal_type(eve1a) # revealed: Person
104106
reveal_type(eve1b) # revealed: Person
105107

106108
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
109+
# error: [invalid-assignment]
107110
eve2a: Person = {"age": 22}
111+
108112
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
109113
eve2b = Person(age=22)
110114

111-
# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
112-
reveal_type(eve2a) # revealed: dict[Unknown | str, Unknown | int]
115+
reveal_type(eve2a) # revealed: Person
113116
reveal_type(eve2b) # revealed: Person
114117

115118
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
119+
# error: [invalid-assignment]
116120
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
121+
117122
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
118123
eve3b = Person(name="Eve", age=25, extra=True)
119124

120-
# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
121-
reveal_type(eve3a) # revealed: dict[Unknown | str, Unknown | str | int]
125+
reveal_type(eve3a) # revealed: Person
122126
reveal_type(eve3b) # revealed: Person
123127
```
124128

@@ -201,6 +205,8 @@ reveal_type(alice["inner"]["age"]) # revealed: int | None
201205
reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown
202206

203207
# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "extra""
208+
# error: [invalid-argument-type]
209+
# error: [invalid-assignment]
204210
alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}}
205211
```
206212

@@ -237,64 +243,88 @@ All of these are missing the required `age` field:
237243

238244
```py
239245
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
246+
# error: [invalid-assignment]
240247
alice2: Person = {"name": "Alice"}
248+
241249
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
242250
Person(name="Alice")
251+
243252
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
244253
Person({"name": "Alice"})
245254

246255
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
256+
# error: [invalid-argument-type]
247257
accepts_person({"name": "Alice"})
248258

249-
# TODO: this should be an error, similar to the above
259+
# TODO: this should be an invalid-key error, similar to the above
260+
# error: [invalid-assignment]
250261
house.owner = {"name": "Alice"}
251262

252263
a_person: Person
253264
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
265+
# error: [invalid-assignment]
254266
a_person = {"name": "Alice"}
255267
```
256268

257269
All of these have an invalid type for the `name` field:
258270

259271
```py
260272
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
273+
# error: [invalid-assignment]
261274
alice3: Person = {"name": None, "age": 30}
275+
262276
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
263277
Person(name=None, age=30)
278+
264279
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
265280
Person({"name": None, "age": 30})
266281

267282
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
283+
# error: [invalid-argument-type]
268284
accepts_person({"name": None, "age": 30})
269-
# TODO: this should be an error, similar to the above
285+
286+
# TODO: this should be an invalid-key error
287+
# error: [invalid-assignment]
270288
house.owner = {"name": None, "age": 30}
271289

272290
a_person: Person
273291
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
292+
# error: [invalid-assignment]
274293
a_person = {"name": None, "age": 30}
294+
275295
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
296+
# error: [invalid-assignment]
276297
(a_person := {"name": None, "age": 30})
277298
```
278299

279300
All of these have an extra field that is not defined in the `TypedDict`:
280301

281302
```py
282303
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
304+
# error: [invalid-assignment]
283305
alice4: Person = {"name": "Alice", "age": 30, "extra": True}
306+
284307
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
285308
Person(name="Alice", age=30, extra=True)
309+
286310
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
287311
Person({"name": "Alice", "age": 30, "extra": True})
288312

289313
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
314+
# error: [invalid-argument-type]
290315
accepts_person({"name": "Alice", "age": 30, "extra": True})
291-
# TODO: this should be an error
316+
317+
# TODO: this should be an invalid-key error
318+
# error: [invalid-assignment]
292319
house.owner = {"name": "Alice", "age": 30, "extra": True}
293320

294321
a_person: Person
295322
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
323+
# error: [invalid-assignment]
296324
a_person = {"name": "Alice", "age": 30, "extra": True}
325+
297326
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
327+
# error: [invalid-assignment]
298328
(a_person := {"name": "Alice", "age": 30, "extra": True})
299329
```
300330

@@ -760,6 +790,7 @@ class Employee(Person):
760790
alice: Employee = {"name": "Alice", "employee_id": 1}
761791

762792
# error: [missing-typed-dict-key] "Missing required key 'employee_id' in TypedDict `Employee` constructor"
793+
# error: [invalid-assignment]
763794
eve: Employee = {"name": "Eve"}
764795

765796
def combine(p: Person, e: Employee):
@@ -861,6 +892,7 @@ p1: TaggedData[int] = {"data": 42, "tag": "number"}
861892
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
862893

863894
# error: [invalid-argument-type] "Invalid argument to key "data" with declared type `int` on TypedDict `TaggedData`: value of type `Literal["not a number"]`"
895+
# error: [invalid-assignment]
864896
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
865897

866898
class Items(TypedDict, Generic[T]):
@@ -894,6 +926,7 @@ p1: TaggedData[int] = {"data": 42, "tag": "number"}
894926
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
895927

896928
# error: [invalid-argument-type] "Invalid argument to key "data" with declared type `int` on TypedDict `TaggedData`: value of type `Literal["not a number"]`"
929+
# error: [invalid-assignment]
897930
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
898931

899932
class Items[T](TypedDict):
@@ -928,6 +961,9 @@ grandchild: Node = {"name": "grandchild", "parent": child}
928961
nested: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": "n3", "parent": None}}}
929962

930963
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Node`: value of type `Literal[3]`"
964+
# error: [invalid-assignment]
965+
# error: [invalid-argument-type]
966+
# error: [invalid-argument-type]
931967
nested_invalid: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": 3, "parent": None}}}
932968
```
933969

@@ -977,7 +1013,7 @@ class Person(TypedDict):
9771013
name: str
9781014
age: int | None
9791015

980-
# TODO: this should be an error
1016+
# error: [invalid-assignment] "Object of type `MyDict` is not assignable to `Person`"
9811017
x: Person = MyDict({"name": "Alice", "age": 30})
9821018
```
9831019

@@ -1029,8 +1065,12 @@ def write_to_non_literal_string_key(person: Person, str_key: str):
10291065
person[str_key] = "Alice" # error: [invalid-key]
10301066

10311067
def create_with_invalid_string_key():
1032-
alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
1033-
bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
1068+
# error: [invalid-key]
1069+
# error: [invalid-assignment]
1070+
alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
1071+
1072+
# error: [invalid-key]
1073+
bob = Person(name="Bob", age=25, unknown="Bar")
10341074
```
10351075

10361076
Assignment to `ReadOnly` keys:

crates/ty_python_semantic/src/types.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1975,11 +1975,14 @@ impl<'db> Type<'db> {
19751975
ConstraintSet::from(false)
19761976
}
19771977

1978-
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
1978+
(Type::TypedDict(_), _) => {
19791979
// TODO: Implement assignability and subtyping for TypedDict
19801980
ConstraintSet::from(relation.is_assignability())
19811981
}
19821982

1983+
// A non-`TypedDict` cannot subtype a `TypedDict`
1984+
(_, Type::TypedDict(_)) => ConstraintSet::from(false),
1985+
19831986
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
19841987
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
19851988
(left, Type::AlwaysFalsy) => ConstraintSet::from(left.bool(db).is_always_false()),

0 commit comments

Comments
 (0)