From 1aaa0847abdebfe910513b1c883977a996da2db5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 4 Sep 2025 17:55:42 +0200 Subject: [PATCH] [ty] More tests for TypedDict (#20205) ## Summary A small set of additional tests for `TypedDict` that I wrote while going through the spec. Note that this certainly doesn't make the test suite exhaustive (see remaining open points in the updated list here: https://github.com/astral-sh/ty/issues/154). --- .../resources/mdtest/typed_dict.md | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index d5544b3d79..9c498e501c 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -128,6 +128,9 @@ Person({"name": "Alice"}) accepts_person({"name": "Alice"}) # TODO: this should be an error, similar to the above house.owner = {"name": "Alice"} +a_person: Person +# TODO: this should be an error, similar to the above +a_person = {"name": "Alice"} ``` All of these have an invalid type for the `name` field: @@ -144,6 +147,9 @@ Person({"name": None, "age": 30}) accepts_person({"name": None, "age": 30}) # TODO: this should be an error, similar to the above house.owner = {"name": None, "age": 30} +a_person: Person +# TODO: this should be an error, similar to the above +a_person = {"name": None, "age": 30} ``` All of these have an extra field that is not defined in the `TypedDict`: @@ -160,6 +166,9 @@ Person({"name": "Alice", "age": 30, "extra": True}) accepts_person({"name": "Alice", "age": 30, "extra": True}) # TODO: this should be an error house.owner = {"name": "Alice", "age": 30, "extra": True} +# TODO: this should be an error +a_person: Person +a_person = {"name": "Alice", "age": 30, "extra": True} ``` ## Type ignore compatibility issues @@ -242,8 +251,9 @@ invalid_extra = OptionalPerson(name="George", extra=True) ## `Required` and `NotRequired` -You can have fine-grained control over field requirements using `Required` and `NotRequired` -qualifiers, which override the class-level `total=` setting: +You can have fine-grained control over keys using `Required` and `NotRequired` qualifiers. These +qualifiers override the class-level `total` setting, which sets the default (`total=True` means that +all keys are required by default, `total=False` means that all keys are non-required by default): ```py from typing_extensions import TypedDict, Required, NotRequired @@ -444,6 +454,12 @@ class Person(TypedDict, total=False): id: ReadOnly[Required[int]] name: str age: int | None + +alice: Person = {"id": 1, "name": "Alice", "age": 30} +alice["age"] = 31 # okay + +# TODO: this should be an error +alice["id"] = 2 ``` ## Methods on `TypedDict` @@ -764,6 +780,38 @@ from typing import TypedDict x: TypedDict = {"name": "Alice"} ``` +### `dict`-subclass inhabitants + +Values that inhabit a `TypedDict` type must be instances of `dict` itself, not a subclass: + +```py +from typing import TypedDict + +class MyDict(dict): + pass + +class Person(TypedDict): + name: str + age: int | None + +# TODO: this should be an error +x: Person = MyDict({"name": "Alice", "age": 30}) +``` + +### Cannot be used in `isinstance` tests + +```py +from typing import TypedDict + +class Person(TypedDict): + name: str + age: int | None + +def _(obj: object) -> bool: + # TODO: this should be an error + return isinstance(obj, Person) +``` + ## Diagnostics