-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] validate constructor call of TypedDict
#19810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
TypedDict
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-08-25 12:37:55.634200073 +0000
+++ new-output.txt 2025-08-25 12:37:58.166200560 +0000
@@ -847,8 +847,16 @@
tuples_type_form.py:15:1: error[invalid-assignment] Object of type `tuple[Literal[1], Literal[""]]` is not assignable to `tuple[int, int]`
tuples_type_form.py:25:1: error[invalid-assignment] Object of type `tuple[Literal[1]]` is not assignable to `tuple[()]`
tuples_type_form.py:36:1: error[invalid-assignment] Object of type `tuple[Literal[1], Literal[2], Literal[3], Literal[""]]` is not assignable to `tuple[int, ...]`
+typeddicts_operations.py:37:5: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
typeddicts_operations.py:60:1: error[type-assertion-failure] Argument does not have asserted type `str | None`
+typeddicts_operations.py:62:1: error[unresolved-attribute] Type `MovieOptional` has no attribute `clear`
+typeddicts_readonly_inheritance.py:65:1: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `RequiredName` constructor
+typeddicts_type_consistency.py:69:21: error[invalid-key] Invalid key access on TypedDict `A3`: Unknown key "y"
typeddicts_type_consistency.py:101:1: error[invalid-assignment] Object of type `Unknown | None` is not assignable to `str`
+typeddicts_usage.py:23:7: error[invalid-key] Invalid key access on TypedDict `Movie`: Unknown key "director"
+typeddicts_usage.py:24:17: error[invalid-assignment] Invalid assignment to key "year" with declared type `int` on TypedDict `Movie`: value of type `Literal["1982"]`
+typeddicts_usage.py:28:1: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
+typeddicts_usage.py:28:18: error[invalid-key] Invalid key access on TypedDict `Movie`: Unknown key "title"
typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 851 diagnostics
+Found 859 diagnostics
WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details. |
|
bea85c3 to
53eb8a7
Compare
53eb8a7 to
39211e1
Compare
75249de to
f006e24
Compare
|
This is very cool. Sorry I haven't gotten a chance to review it yet; will try to soon, or may leave it at this point for @sharkdp to look at next week when he's back from vacation. |
699ea31 to
3fdbe59
Compare
|
Perfect! No problem! Thanks |
|
I'll let @sharkdp take the lead on review of this one. We won't be able to fully evaluate the PR until it is updated with main (so we can get ecosystem and conformance suite results from it), but it should be possible to give feedback on the implementation. |
|
Thank you very much for the detailed updates! I am planning to do a full review on Monday. Seeing the updated ecosystem results would be great, but I can also fix the conflicts myself, if the branch hasn't been updated by then. |
|
@sharkdp I just pushed the merge conflict. I won't be there later today. Feel free to change directly whatever you want. |
5c86699 to
5339975
Compare
sharkdp
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much. This is great.
The typing conformance diff shows that there is a problem with annotations like y: ReadOnly[NotRequired[str]], where we currently emit an error ("Type qualifier typing.NotRequired is not allowed in type expressions"). I think that can be fixed by recognizing ReadOnly as a special form / type qualifier as well, making sure that the argument is inferred as a type annotation, not as a type expression. I can look into this (and the remaining minor comments).
e6d4d3b to
4b2611b
Compare
## Summary Implement validation for `TypedDict` constructor calls and dictionary literal assignments, including support for `total=False` and proper field management. Also add support for `Required` and `NotRequired` type qualifiers in `TypedDict` classes, along with proper inheritance behavior and the `total=` parameter. Support both constructor calls and dict literal syntax part of astral-sh/ty#154 ### Basic Required Field Validation ```py class Person(TypedDict): name: str age: int | None # Error: Missing required field 'name' in TypedDict `Person` constructor incomplete = Person(age=25) # Error: Invalid argument to key "name" with declared type `str` on TypedDict `Person` wrong_type = Person(name=123, age=25) # Error: Invalid key access on TypedDict `Person`: Unknown key "extra" extra_field = Person(name="Bob", age=25, extra=True) ``` <img width="773" height="191" alt="Screenshot 2025-08-07 at 17 59 22" src="https://github.com/user-attachments/assets/79076d98-e85f-4495-93d6-a731aa72a5c9" /> ### Support for `total=False` ```py class OptionalPerson(TypedDict, total=False): name: str age: int | None # All valid - all fields are optional with total=False charlie = OptionalPerson() david = OptionalPerson(name="David") emily = OptionalPerson(age=30) frank = OptionalPerson(name="Frank", age=25) # But type validation and extra fields still apply invalid_type = OptionalPerson(name=123) # Error: Invalid argument type invalid_extra = OptionalPerson(extra=True) # Error: Invalid key access ``` ### Dictionary Literal Validation ```py # Type checking works for both constructors and dict literals person: Person = {"name": "Alice", "age": 30} reveal_type(person["name"]) # revealed: str reveal_type(person["age"]) # revealed: int | None # Error: Invalid key access on TypedDict `Person`: Unknown key "non_existing" reveal_type(person["non_existing"]) # revealed: Unknown ``` ### `Required`, `NotRequired`, `total` ```python from typing import TypedDict from typing_extensions import Required, NotRequired class PartialUser(TypedDict, total=False): name: Required[str] # Required despite total=False age: int # Optional due to total=False email: NotRequired[str] # Explicitly optional (redundant) class User(TypedDict): name: Required[str] # Explicitly required (redundant) age: int # Required due to total=True bio: NotRequired[str] # Optional despite total=True # Valid constructions partial = PartialUser(name="Alice") # name required, age optional full = User(name="Bob", age=25) # name and age required, bio optional # Inheritance maintains original field requirements class Employee(PartialUser): department: str # Required (new field) # name: still Required (inherited) # age: still optional (inherited) emp = Employee(name="Charlie", department="Engineering") # ✅ Employee(department="Engineering") # ❌ e: Employee = {"age": 1} # ❌ ``` <img width="898" height="683" alt="Screenshot 2025-08-11 at 22 02 57" src="https://github.com/user-attachments/assets/4c1b18cd-cb2e-493a-a948-51589d121738" /> ## Implementation The implementation reuses existing validation logic done in astral-sh#19782 ### ℹ️ Why I did NOT synthesize an `__init__` for `TypedDict`: `TypedDict` inherits `dict.__init__(self, *args, **kwargs)` that accepts all arguments. The type resolution system finds this inherited signature **before** looking for synthesized members. So `own_synthesized_member()` is never called because a signature already exists. To force synthesis, you'd have to override Python’s inheritance mechanism, which would break compatibility with the existing ecosystem. This is why I went with ad-hoc validation. IMO it's the only viable approach that respects Python’s inheritance semantics while providing the required validation. ### Refacto of `Field` **Before:** ```rust struct Field<'db> { declared_ty: Type<'db>, default_ty: Option<Type<'db>>, // NamedTuple and dataclass only init_only: bool, // dataclass only init: bool, // dataclass only is_required: Option<bool>, // TypedDict only } ``` **After:** ```rust struct Field<'db> { declared_ty: Type<'db>, kind: FieldKind<'db>, } enum FieldKind<'db> { NamedTuple { default_ty: Option<Type<'db>> }, Dataclass { default_ty: Option<Type<'db>>, init_only: bool, init: bool }, TypedDict { is_required: bool }, } ``` ## Test Plan Updated Markdown tests --------- Co-authored-by: David Peter <mail@david-peter.de>
Summary
Implement validation for
TypedDictconstructor calls and dictionary literal assignments, including support fortotal=Falseand proper field management.Also add support for
RequiredandNotRequiredtype qualifiers inTypedDictclasses, along with proper inheritance behavior and thetotal=parameter.Support both constructor calls and dict literal syntax
part of astral-sh/ty#154
Basic Required Field Validation
Support for
total=FalseDictionary Literal Validation
Required,NotRequired,totalImplementation
The implementation reuses existing validation logic done in #19782
ℹ️ Why I did NOT synthesize an
__init__forTypedDict:TypedDictinheritsdict.__init__(self, *args, **kwargs)that accepts all arguments.The type resolution system finds this inherited signature before looking for synthesized members.
So
own_synthesized_member()is never called because a signature already exists.To force synthesis, you'd have to override Python’s inheritance mechanism, which would break compatibility with the existing ecosystem.
This is why I went with ad-hoc validation. IMO it's the only viable approach that respects Python’s
inheritance semantics while providing the required validation.
Refacto of
FieldBefore:
After:
Test Plan
Updated Markdown tests