diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 858f1f0c7c..4218eee1af 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -474,7 +474,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -599,7 +599,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -650,7 +650,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -679,7 +679,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -723,7 +723,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -762,11 +762,15 @@ Added in 0
**What it does**
-Checks for subscript accesses with invalid keys.
+Checks for subscript accesses with invalid keys and `TypedDict` construction with an
+unknown key.
**Why is this bad?**
-Using an invalid key will raise a `KeyError` at runtime.
+Subscripting with an invalid key will raise a `KeyError` at runtime.
+
+Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is
+`closed=true` it also violates the expectations of the type.
**Examples**
@@ -779,6 +783,10 @@ class Person(TypedDict):
alice = Person(name="Alice", age=30)
alice["height"] # KeyError: 'height'
+
+bob: Person = { "name": "Bob", "age": 30 } # typo!
+
+carol = Person(name="Carol", age=25) # typo!
```
## `invalid-legacy-type-variable`
@@ -787,7 +795,7 @@ alice["height"] # KeyError: 'height'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -822,7 +830,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -888,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -938,7 +946,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -998,7 +1006,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1047,7 +1055,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1072,7 +1080,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1130,7 +1138,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1157,7 +1165,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1187,7 +1195,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1217,7 +1225,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1251,7 +1259,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1285,7 +1293,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1320,7 +1328,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1345,7 +1353,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1378,7 +1386,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1407,7 +1415,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1431,7 +1439,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1457,7 +1465,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1484,7 +1492,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1542,7 +1550,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1572,7 +1580,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1601,7 +1609,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1628,7 +1636,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1656,7 +1664,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1702,7 +1710,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1729,7 +1737,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1757,7 +1765,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1782,7 +1790,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1807,7 +1815,7 @@ print(x) # NameError: name 'x' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1844,7 +1852,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1872,7 +1880,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2026,7 +2034,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2086,7 +2094,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2118,7 +2126,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2145,7 +2153,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2169,7 +2177,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2227,7 +2235,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2266,7 +2274,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2353,7 +2361,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
index b80700fa08..155b4ea618 100644
--- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
@@ -37,20 +37,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
23 |
24 | def write_to_non_literal_string_key(person: Person, str_key: str):
25 | person[str_key] = "Alice" # error: [invalid-key]
-26 | from typing_extensions import ReadOnly
-27 |
-28 | class Employee(TypedDict):
-29 | id: ReadOnly[int]
-30 | name: str
+26 |
+27 | def create_with_invalid_string_key():
+28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
+29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
+30 | from typing_extensions import ReadOnly
31 |
-32 | def write_to_readonly_key(employee: Employee):
-33 | employee["id"] = 42 # error: [invalid-assignment]
+32 | class Employee(TypedDict):
+33 | id: ReadOnly[int]
+34 | name: str
+35 |
+36 | def write_to_readonly_key(employee: Employee):
+37 | employee["id"] = 42 # error: [invalid-assignment]
```
# Diagnostics
```
-error[invalid-key]: Invalid key access on TypedDict `Person`
+error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:8:5
|
7 | def access_invalid_literal_string_key(person: Person):
@@ -66,7 +70,7 @@ info: rule `invalid-key` is enabled by default
```
```
-error[invalid-key]: Invalid key access on TypedDict `Person`
+error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:13:5
|
12 | def access_invalid_key(person: Person):
@@ -82,7 +86,7 @@ info: rule `invalid-key` is enabled by default
```
```
-error[invalid-key]: TypedDict `Person` cannot be indexed with a key of type `str`
+error[invalid-key]: Invalid key for TypedDict `Person` of type `str`
--> src/mdtest_snippet.py:16:12
|
15 | def access_with_str_key(person: Person, str_key: str):
@@ -123,7 +127,7 @@ info: rule `invalid-assignment` is enabled by default
```
```
-error[invalid-key]: Invalid key access on TypedDict `Person`
+error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:22:5
|
21 | def write_to_non_existing_key(person: Person):
@@ -145,7 +149,39 @@ error[invalid-key]: Cannot access `Person` with a key of type `str`. Only string
24 | def write_to_non_literal_string_key(person: Person, str_key: str):
25 | person[str_key] = "Alice" # error: [invalid-key]
| ^^^^^^^
-26 | from typing_extensions import ReadOnly
+26 |
+27 | def create_with_invalid_string_key():
+ |
+info: rule `invalid-key` is enabled by default
+
+```
+
+```
+error[invalid-key]: Invalid key for TypedDict `Person`
+ --> src/mdtest_snippet.py:28:21
+ |
+27 | def create_with_invalid_string_key():
+28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
+ | -----------------------------^^^^^^^^^--------
+ | | |
+ | | Unknown key "unknown"
+ | TypedDict `Person`
+29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
+30 | from typing_extensions import ReadOnly
+ |
+info: rule `invalid-key` is enabled by default
+
+```
+
+```
+error[invalid-key]: Invalid key for TypedDict `Person`
+ --> src/mdtest_snippet.py:29:11
+ |
+27 | def create_with_invalid_string_key():
+28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
+29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
+ | ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown"
+30 | from typing_extensions import ReadOnly
|
info: rule `invalid-key` is enabled by default
@@ -153,21 +189,21 @@ info: rule `invalid-key` is enabled by default
```
error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee`
- --> src/mdtest_snippet.py:33:5
+ --> src/mdtest_snippet.py:37:5
|
-32 | def write_to_readonly_key(employee: Employee):
-33 | employee["id"] = 42 # error: [invalid-assignment]
+36 | def write_to_readonly_key(employee: Employee):
+37 | employee["id"] = 42 # error: [invalid-assignment]
| -------- ^^^^ key is marked read-only
| |
| TypedDict `Employee`
|
info: Item declaration
- --> src/mdtest_snippet.py:29:5
+ --> src/mdtest_snippet.py:33:5
|
-28 | class Employee(TypedDict):
-29 | id: ReadOnly[int]
+32 | class Employee(TypedDict):
+33 | id: ReadOnly[int]
| ----------------- Read-only item declared here
-30 | name: str
+34 | name: str
|
info: rule `invalid-assignment` is enabled by default
diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
index d810a79efe..042d6317a2 100644
--- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md
+++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
@@ -29,7 +29,7 @@ alice: Person = {"name": "Alice", "age": 30}
reveal_type(alice["name"]) # revealed: str
reveal_type(alice["age"]) # revealed: int | None
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(alice["non_existing"]) # revealed: Unknown
```
@@ -41,7 +41,7 @@ bob = Person(name="Bob", age=25)
reveal_type(bob["name"]) # revealed: str
reveal_type(bob["age"]) # revealed: int | None
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(bob["non_existing"]) # revealed: Unknown
```
@@ -69,7 +69,7 @@ def name_or_age() -> Literal["name", "age"]:
carol: Person = {NAME: "Carol", AGE: 20}
reveal_type(carol[NAME]) # revealed: str
-# error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`"
+# error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`"
reveal_type(carol[non_literal()]) # revealed: Unknown
reveal_type(carol[name_or_age()]) # revealed: str | int | None
@@ -81,7 +81,7 @@ def _():
CAPITALIZED_NAME = "Name"
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20}
@@ -104,9 +104,9 @@ eve2a: Person = {"age": 22}
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
eve2b = Person(age=22)
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
eve3b = Person(name="Eve", age=25, extra=True)
```
@@ -157,10 +157,10 @@ bob["name"] = None
Assignments to non-existing keys are disallowed:
```py
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
alice["extra"] = True
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
bob["extra"] = True
```
@@ -185,10 +185,10 @@ alice: Person = {"inner": {"name": "Alice", "age": 30}}
reveal_type(alice["inner"]["name"]) # revealed: str
reveal_type(alice["inner"]["age"]) # revealed: int | None
-# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "non_existing""
+# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "non_existing""
reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown
-# error: [invalid-key] "Invalid key access on TypedDict `Inner`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "extra""
alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}}
```
@@ -267,22 +267,22 @@ a_person = {"name": None, "age": 30}
All of these have an extra field that is not defined in the `TypedDict`:
```py
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
alice4: Person = {"name": "Alice", "age": 30, "extra": True}
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
Person(name="Alice", age=30, extra=True)
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
Person({"name": "Alice", "age": 30, "extra": True})
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
accepts_person({"name": "Alice", "age": 30, "extra": True})
# TODO: this should be an error
house.owner = {"name": "Alice", "age": 30, "extra": True}
a_person: Person
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
a_person = {"name": "Alice", "age": 30, "extra": True}
-# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
(a_person := {"name": "Alice", "age": 30, "extra": True})
```
@@ -323,7 +323,7 @@ user2 = User({"name": "Bob"})
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `User`: value of type `None`"
user3 = User({"name": None, "age": 25})
-# error: [invalid-key] "Invalid key access on TypedDict `User`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `User`: Unknown key "extra""
user4 = User({"name": "Charlie", "age": 30, "extra": True})
```
@@ -360,7 +360,7 @@ invalid = OptionalPerson(name=123)
Extra fields are still not allowed, even with `total=False`:
```py
-# error: [invalid-key] "Invalid key access on TypedDict `OptionalPerson`: Unknown key "extra""
+# error: [invalid-key] "Invalid key for TypedDict `OptionalPerson`: Unknown key "extra""
invalid_extra = OptionalPerson(name="George", extra=True)
```
@@ -503,10 +503,10 @@ def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age",
reveal_type(person[union_of_keys]) # revealed: int | None | str
- # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
+ # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(person["non_existing"]) # revealed: Unknown
- # error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`"
+ # error: [invalid-key] "Invalid key for TypedDict `Person` of type `str`"
reveal_type(person[str_key]) # revealed: Unknown
# No error here:
@@ -530,7 +530,7 @@ def _(person: Person):
person["name"] = "Alice"
person["age"] = 30
- # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "naem" - did you mean "name"?"
+ # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "naem" - did you mean "name"?"
person["naem"] = "Alice"
def _(person: Person):
@@ -646,7 +646,7 @@ def _(p: Person) -> None:
reveal_type(p.setdefault("name", "Alice")) # revealed: str
reveal_type(p.setdefault("extra", "default")) # revealed: str
- # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
+ # error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown
```
@@ -1015,6 +1015,10 @@ def write_to_non_existing_key(person: Person):
def write_to_non_literal_string_key(person: Person, str_key: str):
person[str_key] = "Alice" # error: [invalid-key]
+
+def create_with_invalid_string_key():
+ alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
+ bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
```
Assignment to `ReadOnly` keys:
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 7db83b9b88..2dd75e57aa 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -572,10 +572,14 @@ declare_lint! {
// Added in #19763.
declare_lint! {
/// ## What it does
- /// Checks for subscript accesses with invalid keys.
+ /// Checks for subscript accesses with invalid keys and `TypedDict` construction with an
+ /// unknown key.
///
/// ## Why is this bad?
- /// Using an invalid key will raise a `KeyError` at runtime.
+ /// Subscripting with an invalid key will raise a `KeyError` at runtime.
+ ///
+ /// Creating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is
+ /// `closed=true` it also violates the expectations of the type.
///
/// ## Examples
/// ```python
@@ -587,9 +591,13 @@ declare_lint! {
///
/// alice = Person(name="Alice", age=30)
/// alice["height"] # KeyError: 'height'
+ ///
+ /// bob: Person = { "name": "Bob", "age": 30 } # typo!
+ ///
+ /// carol = Person(name="Carol", age=25) # typo!
/// ```
pub(crate) static INVALID_KEY = {
- summary: "detects invalid subscript accesses",
+ summary: "detects invalid subscript accesses or TypedDict literal keys",
status: LintStatus::stable("0.0.1-alpha.17"),
default_level: Level::Error,
}
@@ -2966,7 +2974,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
let typed_dict_name = typed_dict_ty.display(db);
let mut diagnostic = builder.into_diagnostic(format_args!(
- "Invalid key access on TypedDict `{typed_dict_name}`",
+ "Invalid key for TypedDict `{typed_dict_name}`",
));
diagnostic.annotate(
@@ -2989,7 +2997,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
diagnostic
}
_ => builder.into_diagnostic(format_args!(
- "TypedDict `{}` cannot be indexed with a key of type `{}`",
+ "Invalid key for TypedDict `{}` of type `{}`",
typed_dict_ty.display(db),
key_ty.display(db),
)),
diff --git a/ty.schema.json b/ty.schema.json
index 270241fb28..55d5bdf996 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -584,8 +584,8 @@
]
},
"invalid-key": {
- "title": "detects invalid subscript accesses",
- "description": "## What it does\nChecks for subscript accesses with invalid keys.\n\n## Why is this bad?\nUsing an invalid key will raise a `KeyError` at runtime.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n```",
+ "title": "detects invalid subscript accesses or TypedDict literal keys",
+ "description": "## What it does\nChecks for subscript accesses with invalid keys and `TypedDict` construction with an\nunknown key.\n\n## Why is this bad?\nSubscripting with an invalid key will raise a `KeyError` at runtime.\n\nCreating a `TypedDict` with an unknown key is likely a mistake; if the `TypedDict` is\n`closed=true` it also violates the expectations of the type.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n\nbob: Person = { \"name\": \"Bob\", \"age\": 30 } # typo!\n\ncarol = Person(name=\"Carol\", age=25) # typo!\n```",
"default": "error",
"oneOf": [
{