Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/typed_dict.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,38 @@ accepts_person({"name": "Alice", "age": 30})
house.owner = {"name": "Alice", "age": 30}
```

Keyword arguments should override a positional mapping, and `TypedDict` constructor inputs should
preserve shared required keys:

```py
from typing import TypedDict

class ChildWithOptionalCount(TypedDict, total=False):
count: int

ChildWithOptionalCount({"count": "wrong"}, count=1)

class Base(TypedDict):
name: str

class ChildKwargs(TypedDict):
name: str
count: int

class MaybeName(TypedDict, total=False):
name: str

def _(
base: Base,
maybe_name: MaybeName,
):
ChildKwargs(base, count=1)
ChildKwargs(**base, count=1)

# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `ChildKwargs` constructor"
ChildKwargs(**maybe_name, count=1)
```

All of these are missing the required `age` field:

```py
Expand Down Expand Up @@ -523,6 +555,28 @@ a_person = {"name": "Alice", "age": 30, "extra": True}
(a_person := {"name": "Alice", "age": 30, "extra": True})
```

## Mixed positional and unpacked keyword constructors

These calls mix a positional `TypedDict` argument with unpacked keyword arguments. They should
validate normally and produce ordinary diagnostics:

```py
from typing import TypedDict

class MixedTarget(TypedDict):
x: int
y: int

class MaybeY(TypedDict, total=False):
y: int

def _(target: MixedTarget, maybe_y: MaybeY):
MixedTarget(target, **maybe_y)

# error: [missing-typed-dict-key] "Missing required key 'y' in TypedDict `MixedTarget` constructor"
MixedTarget({"x": 1}, **maybe_y)
```

## Union of `TypedDict`

When assigning to a union of `TypedDict` types, the type will be narrowed based on the dictionary
Expand Down Expand Up @@ -1989,6 +2043,104 @@ def accepts_typed_dict_class(t_person: type[Person]) -> None:
accepts_typed_dict_class(Person)
```

Calling a union of `TypedDict` class objects validates each constructor arm:

```py
from typing import TypedDict

class Foo(TypedDict):
a: int

class Bar(TypedDict):
a: int

def _(t: type[Foo | Bar]) -> None:
# error: [invalid-argument-type] "Invalid argument to key "a" with declared type `int` on TypedDict `Foo`: value of type `Literal["baz"]`"
# error: [invalid-argument-type] "Invalid argument to key "a" with declared type `int` on TypedDict `Bar`: value of type `Literal["baz"]`"
t(a="baz")
```

## Constrained class-object constructors

Constrained `type[T]` constructor calls keep the relationship between the constructor target and
other arguments typed as the same `T`:

```py
from typing import TypeVar, TypedDict

class Foo(TypedDict):
foo: int

class Bar(TypedDict):
bar: int

T = TypeVar("T", Foo, Bar)

def _(t: type[T], kwargs: T) -> None:
t(**kwargs)
```

The same unpacked key information should be preserved at ordinary `**kwargs` call sites:

```py
from typing import TypeVar, TypedDict

class Foo(TypedDict):
foo: int

class Bar(TypedDict):
bar: str

class MaybeFoo(TypedDict, total=False):
foo: int

class BadBar(TypedDict):
bar: int

T = TypeVar("T", Foo, Bar)
U = TypeVar("U", Foo, BadBar)

def needs_both(*, foo: int, bar: str) -> None:
pass

def needs_foo(*, foo: int) -> None:
pass

def foo_only(*, foo: int = 0) -> None:
pass

def typed_bar(*, foo: int = 0, bar: str = "") -> None:
pass

def _(kwargs: T, maybe_foo: MaybeFoo, other_kwargs: U) -> None:
# error: [missing-argument] "No arguments provided for required parameters `foo`, `bar` of function `needs_both`"
needs_both(**kwargs)

# error: [missing-argument] "No argument provided for required parameter `foo` of function `needs_foo`"
needs_foo(**maybe_foo)

# error: [unknown-argument] "Argument `bar` does not match any known parameter of function `foo_only`"
foo_only(**kwargs)

# error: [invalid-argument-type] "Argument to function `typed_bar` is incorrect"
typed_bar(**other_kwargs)
```

Upper-bounded `type[T]` constructor calls still validate the bound `TypedDict` schema:

```py
from typing import TypeVar, TypedDict

class NeedsInt(TypedDict):
a: int

T = TypeVar("T", bound=NeedsInt)

def _(t: type[T]) -> None:
# error: [invalid-argument-type] "Invalid argument to key "a" with declared type `int` on TypedDict `NeedsInt`: value of type `Literal["x"]`"
t(a="x")
```

## Subclassing

`TypedDict` types can be subclassed. The subclass can add new keys:
Expand Down Expand Up @@ -2398,6 +2550,32 @@ def _(node: Node, person: Person):
_: Node = Person(name="Alice", parent=Node(name="Bob", parent=Person(name="Charlie", parent=None)))
```

TypedDict constructor calls should also use field type context when inferring nested values:

```py
from typing import TypedDict

class Comparison(TypedDict):
field: str
value: object

class Logical(TypedDict):
primary: Comparison
conditions: list[Comparison]

logical_from_literal = Logical(
primary=Comparison(field="a", value="b"),
conditions=[Comparison(field="c", value="d")],
)
logical_from_dict_call = Logical(dict(primary=dict(field="a", value="b"), conditions=[dict(field="c", value="d")]))

# error: [missing-typed-dict-key]
missing_primary_from_dict_call = Logical(primary=dict(field="a"), conditions=[dict(field="c", value="d")])

# error: [missing-typed-dict-key]
missing_primary_from_literal = Logical(primary={"field": "a"}, conditions=[dict(field="c", value="d")])
```

## Function/assignment syntax

TypedDicts can be created using the functional syntax:
Expand Down
Loading
Loading