[ty] Validate writes to TypedDict keys by sharkdp · Pull Request #19782 · astral-sh/ruff (original) (raw)
added the ty
Multi-file analysis & type inference
label
sharkdp marked this pull request as ready for review
carljm deleted the david/typeddict-setitem branch
dcreager added a commit that referenced this pull request
- origin/main:
[ty] Implemented support for "rename" language server feature (#19551)
[ty] Reduce size of member table (#19572)
[ty] Move server capabilities creation (#19798)
[ty] Repurpose
FunctionType.into_bound_method_typeto returnBoundMethodType(#19793) [ty] Validate writes toTypedDictkeys (#19782) [ty] Add support for using the test command emitted when a mdtest fails (#19794)
sharkdp added a commit that referenced this pull request
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
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)Support for total=False
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 accessDictionary Literal Validation
# 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: UnknownRequired, NotRequired, total
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} # ❌Implementation
The implementation reuses existing validation logic done in #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:
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:
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
second-ed pushed a commit to second-ed/ruff that referenced this pull request
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
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)Support for total=False
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 accessDictionary Literal Validation
# 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: UnknownRequired, NotRequired, total
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} # ❌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:
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:
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
KotlinIsland pushed a commit to KotlinIsland/basedpython that referenced this pull request
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
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)Support for total=False
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 accessDictionary Literal Validation
# 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: UnknownRequired, NotRequired, total
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} # ❌Implementation
The implementation reuses existing validation logic done in astral-sh/ruff#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:
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:
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
[ Show hidden characters]({{ revealButtonHref }})