How Pyrefly Handles TypedDict total and non-total Requirements
Pyrefly treats TypedDict as a class-based construct where the is_total flag initializes to true and flips based on keyword arguments, with field requirements calculated through metadata merging that respects inheritance constraints and newer closed/extra_items keywords.
The facebook/pyrefly repository implements a Rust-based Python type checker that enforces PEP 589 TypedDict specifications through precise metadata extraction and field-level calculations. Understanding how Pyrefly handles TypedDict total and non-total requirements reveals the mechanisms behind optional field detection, inheritance validation, and strict dictionary key constraints.
Class Metadata Extraction: Parsing total and closed Keywords
Pyrefly extracts TypedDict configuration through the typed_dict_metadata method in pyrefly/lib/alt/class/class_metadata.rs (lines 668‑692). This function walks class-level keyword arguments to determine field requirements and extra-item policies.
The implementation initializes is_total = true as the default state. When encountering total: False, the flag flips to false; conversely, total: True leaves the flag unchanged. This boolean directly determines whether fields become required or optional during the subsequent field calculation phase.
The method also handles newer TypedDict syntax extensions (lines 672‑697). The closed and extra_items keywords are parsed here, with closed=True mapping to ExtraItems::Closed and closed=False yielding ExtraItems::Default. These options are mutually exclusive—supplying both triggers a BadTypedDict error diagnostic.
Field-Level Calculation: Required vs Optional Fields
After metadata extraction, calculate_typed_dict_metadata_fields merges fields from the current class with those inherited from base TypedDicts. This function respects the is_total flag established during the keyword parsing phase.
When is_total is false, every field receives optional status (NotRequired) unless a base class explicitly marks it as Required. Conversely, when is_total remains true, fields default to required unless explicitly annotated otherwise. The TypedDictMetadata struct stores these requirements in a SmallMap<Name, bool> where the boolean tracks the total status per field.
The solver later uses this map to enforce read-only (ReadOnly) and required (Required) markers when deriving concrete types from the TypedDict definition.
Type Conversion Pipeline and Partial Views
When TypedDict definitions enter the type-solver pipeline, pyrefly/lib/tsp/type_conversion.rs (lines 200‑218) converts them into class declarations through a dedicated match arm. Named TypedDicts generate backing class objects, while anonymous TypedDicts fall back to the built-in TypedDict type.
The same conversion logic handles PartialTypedDict, which represents incremental analysis views where certain fields may be temporarily absent. This ensures consistent behavior between named class definitions and inline TypedDict type hints during constraint solving.
Inheritance Rules and Constraint Propagation
Pyrefly walks the base-class chain using bases_with_metadata to merge parent TypedDict constraints with child definitions. The inheritance logic preserves the most restrictive requirements: a child cannot convert a parent-required field into an optional one, even if the child specifies total=False.
This validation appears in the test suite at pyrefly/lib/test/typed_dict.rs (lines 262‑272), which verifies that inheritance violations raise appropriate errors. The merging algorithm combines parent total flags and extra_items markers with child values, ensuring constraint monotonicity across the inheritance hierarchy.
Error Handling and Validation
Invalid TypedDict configurations trigger the BadTypedDict diagnostic. Pyrefly emits errors for:
- Contradictory use of
closedandextra_itemskeywords - Non-literal values supplied for
totalorclosedarguments - Unsupported keywords in the TypedDict definition
- Inheritance violations where child classes attempt to relax required fields from parent classes
Code Examples: total and closed in Practice
from typing import TypedDict, Required, NotRequired
# 1. Default total=True (both fields required)
class Point(TypedDict):
x: int
y: int
# 2. Non-total (total=False makes fields optional)
class PartialPoint(TypedDict, total=False):
x: int
y: int
# 3. Closed TypedDict (rejects extra keys)
class StrictPoint(TypedDict, total=False, closed=True):
x: int
y: int
# 4. Illegal inheritance (parent requires, child cannot make optional)
class Base(TypedDict):
a: int
class Child(Base, total=False): # Error: cannot make 'a' optional
pass
Summary
- Pyrefly initializes
is_total = trueby default, flipping tofalseonly when the class keyword argumenttotal=Falseis explicitly detected inclass_metadata.rs. - Field requirements are calculated through
calculate_typed_dict_metadata_fields, which marks fields asNotRequiredwhenis_totalisfalsewhile preserving explicitRequiredannotations from base classes. - The
closedandextra_itemskeywords are parsed mutually exclusively; contradictory usage raisesBadTypedDicterrors during metadata extraction. - Inheritance constraints propagate through
bases_with_metadata, preventing child classes from relaxing required fields defined in parent TypedDicts. - Type conversion occurs in
type_conversion.rs, handling both named TypedDicts andPartialTypedDictviews for incremental analysis.
Frequently Asked Questions
What happens when a child TypedDict sets total=False but the parent has total=True?
Pyrefly enforces that child classes cannot relax requirements from parent TypedDicts. Even if the child specifies total=False, any fields declared required in the parent remain required in the child. Attempting to override this triggers a BadTypedDict inheritance error, as verified in pyrefly/lib/test/typed_dict.rs.
How does Pyrefly handle the newer closed keyword in TypedDict?
The closed keyword is parsed in typed_dict_metadata (lines 672‑697), where closed=True creates an ExtraItems::Closed marker that rejects undeclared keys, while closed=False uses ExtraItems::Default. This keyword is mutually exclusive with extra_items; using both raises a BadTypedDict diagnostic.
What is PartialTypedDict in Pyrefly's implementation?
PartialTypedDict represents an incremental analysis view where certain fields may be temporarily absent from the dictionary. During type conversion in type_conversion.rs, these partial views follow the same conversion path as standard TypedDicts, ensuring consistent constraint solving during progressive type checking.
How does Pyrefly report errors for invalid TypedDict configurations?
Pyrefly emits BadTypedDict diagnostics for invalid keyword combinations, non-literal values for total or closed, unsupported arguments, and inheritance constraint violations. These errors originate from the metadata extraction phase in class_metadata.rs and surface during class body analysis.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →