mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Initial test suite for TypedDict
(#19686)
## Summary Adds an initial set of tests based on the highest-priority items in https://github.com/astral-sh/ty/issues/154. This is certainly not yet exhaustive (required/non-required, `total`, and other things are missing), but will be useful to measure progress on this feature. ## Test Plan Checked intended behavior against runtime and other type checkers.
This commit is contained in:
parent
e7e7b7bf21
commit
48d5bd13fa
1 changed files with 257 additions and 5 deletions
|
@ -1,16 +1,267 @@
|
||||||
# `TypedDict`
|
# `TypedDict`
|
||||||
|
|
||||||
We do not support `TypedDict`s yet. This test mainly exists to make sure that we do not emit any
|
A [`TypedDict`] type represents dictionary objects with a specific set of string keys, and with
|
||||||
errors for the definition of a `TypedDict`.
|
specific value types for each valid key. Each string key can be either required or non-required.
|
||||||
|
|
||||||
|
## Basic
|
||||||
|
|
||||||
|
Here, we define a `TypedDict` using the class-based syntax:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing_extensions import TypedDict, Required
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int | None
|
||||||
|
```
|
||||||
|
|
||||||
|
New inhabitants can be created from dict literals. When accessing keys, the correct types should be
|
||||||
|
inferred based on the `TypedDict` definition:
|
||||||
|
|
||||||
|
```py
|
||||||
|
alice: Person = {"name": "Alice", "age": 30}
|
||||||
|
|
||||||
|
# TODO: this should be `str`
|
||||||
|
reveal_type(alice["name"]) # revealed: Unknown
|
||||||
|
# TODO: this should be `int | None`
|
||||||
|
reveal_type(alice["age"]) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
Inhabitants can also be created through a constructor call:
|
||||||
|
|
||||||
|
```py
|
||||||
|
bob = Person(name="Bob", age=25)
|
||||||
|
```
|
||||||
|
|
||||||
|
Methods that are available on `dict`s are also available on `TypedDict`s:
|
||||||
|
|
||||||
|
```py
|
||||||
|
bob.update(age=26)
|
||||||
|
```
|
||||||
|
|
||||||
|
The construction of a `TypedDict` is checked for type correctness:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# TODO: these should be errors (invalid argument type)
|
||||||
|
eve1a: Person = {"name": b"Eve", "age": None}
|
||||||
|
eve1b = Person(name=b"Eve", age=None)
|
||||||
|
|
||||||
|
# TODO: these should be errors (missing required key)
|
||||||
|
eve2a: Person = {"age": 22}
|
||||||
|
eve2b = Person(age=22)
|
||||||
|
|
||||||
|
# TODO: these should be errors (additional key)
|
||||||
|
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
|
||||||
|
eve3b = Person(name="Eve", age=25, extra=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
Assignments to keys are also validated:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# TODO: this should be an error
|
||||||
|
alice["name"] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Assignments to non-existing keys are disallowed:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# TODO: this should be an error
|
||||||
|
alice["extra"] = True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structural assignability
|
||||||
|
|
||||||
|
Assignability between `TypedDict` types is structural, that is, it is based on the presence of keys
|
||||||
|
and their types, rather than the class hierarchy:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Employee(TypedDict):
|
||||||
|
name: str
|
||||||
|
employee_id: int
|
||||||
|
|
||||||
|
p1: Person = Employee(name="Alice", employee_id=1)
|
||||||
|
|
||||||
|
# TODO: this should be an error
|
||||||
|
e1: Employee = Person(name="Eve")
|
||||||
|
```
|
||||||
|
|
||||||
|
All typed dictionaries can be assigned to `Mapping[str, object]`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Mapping, TypedDict
|
||||||
|
|
||||||
class Person(TypedDict):
|
class Person(TypedDict):
|
||||||
name: str
|
name: str
|
||||||
age: int | None
|
age: int | None
|
||||||
|
|
||||||
alice: Person = {"name": "Alice", "age": 30}
|
m: Mapping[str, object] = Person(name="Alice", age=30)
|
||||||
|
```
|
||||||
|
|
||||||
|
They can *not* be assigned to `dict[str, object]`, as that would allow them to be mutated in unsafe
|
||||||
|
ways:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
def dangerous(d: dict[str, object]) -> None:
|
||||||
|
d["name"] = 1
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
alice: Person = {"name": "Alice"}
|
||||||
|
|
||||||
|
# TODO: this should be an invalid-assignment error
|
||||||
|
dangerous(alice)
|
||||||
|
|
||||||
|
# TODO: this should be `str`
|
||||||
|
reveal_type(alice["name"]) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types of keys and values
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int | None
|
||||||
|
|
||||||
|
def _(p: Person) -> None:
|
||||||
|
reveal_type(p.keys()) # revealed: @Todo(Support for `TypedDict`)
|
||||||
|
reveal_type(p.values()) # revealed: @Todo(Support for `TypedDict`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unlike normal classes
|
||||||
|
|
||||||
|
`TypedDict` types are not like normal classes. The "attributes" can not be accessed. Neither on the
|
||||||
|
class itself, nor on inhabitants of the type defined by the class:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int | None
|
||||||
|
|
||||||
|
# TODO: this should be an error
|
||||||
|
Person.name
|
||||||
|
|
||||||
|
# TODO: this should be an error
|
||||||
|
Person(name="Alice", age=30).name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special properties
|
||||||
|
|
||||||
|
`TypedDict` class definitions have some special properties that can be used for introspection:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int | None
|
||||||
|
|
||||||
|
reveal_type(Person.__total__) # revealed: @Todo(Support for `TypedDict`)
|
||||||
|
reveal_type(Person.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||||
|
reveal_type(Person.__optional_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subclassing
|
||||||
|
|
||||||
|
`TypedDict` types can be subclassed. The subclass can add new keys:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Person(TypedDict):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Employee(Person):
|
||||||
|
employee_id: int
|
||||||
|
|
||||||
|
alice: Employee = {"name": "Alice", "employee_id": 1}
|
||||||
|
|
||||||
|
# TODO: this should be an error (missing required key)
|
||||||
|
eve: Employee = {"name": "Eve"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic `TypedDict`
|
||||||
|
|
||||||
|
`TypedDict`s can also be generic.
|
||||||
|
|
||||||
|
### Legacy generics
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Generic, TypeVar, TypedDict
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
class TaggedData(TypedDict, Generic[T]):
|
||||||
|
data: T
|
||||||
|
tag: str
|
||||||
|
|
||||||
|
p1: TaggedData[int] = {"data": 42, "tag": "number"}
|
||||||
|
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
|
||||||
|
|
||||||
|
# TODO: this should be an error (type mismatch)
|
||||||
|
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PEP-695 generics
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class TaggedData[T](TypedDict):
|
||||||
|
data: T
|
||||||
|
tag: str
|
||||||
|
|
||||||
|
p1: TaggedData[int] = {"data": 42, "tag": "number"}
|
||||||
|
p2: TaggedData[str] = {"data": "Hello", "tag": "text"}
|
||||||
|
|
||||||
|
# TODO: this should be an error (type mismatch)
|
||||||
|
p3: TaggedData[int] = {"data": "not a number", "tag": "number"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recursive `TypedDict`
|
||||||
|
|
||||||
|
`TypedDict`s can also be recursive, allowing for nested structures:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Node(TypedDict):
|
||||||
|
name: str
|
||||||
|
parent: Node | None
|
||||||
|
|
||||||
|
root: Node = {"name": "root", "parent": None}
|
||||||
|
child: Node = {"name": "child", "parent": root}
|
||||||
|
grandchild: Node = {"name": "grandchild", "parent": child}
|
||||||
|
|
||||||
|
nested: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": "n3", "parent": None}}}
|
||||||
|
|
||||||
|
# TODO: this should be an error (invalid type for `name` in innermost node)
|
||||||
|
nested_invalid: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": 3, "parent": None}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Function/assignment syntax
|
||||||
|
|
||||||
|
This is not yet supported. Make sure that we do not emit false positives for this syntax:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import TypedDict, Required
|
||||||
|
|
||||||
# Alternative syntax
|
# Alternative syntax
|
||||||
Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False)
|
Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False)
|
||||||
|
@ -20,6 +271,7 @@ msg = Message(id=1, content="Hello")
|
||||||
# No errors for yet-unsupported features (`closed`):
|
# No errors for yet-unsupported features (`closed`):
|
||||||
OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True)
|
OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True)
|
||||||
|
|
||||||
reveal_type(Person.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
|
||||||
reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue