Experimental Pydantic Support
Pyrefly includes experimental support for Pydantic, a popular Python library for data validation and parsing. This feature aims to provide improved static type checking and IDE integration for Pydantic models.
Note: This support is experimental and actively evolving. The design may change based on feedback and development. Please note that we are supporting Pydantic v2 and above, which means that deprecated v1 features are not included.
Feedback
We welcome your feedback and suggestions. Please share your thoughts and ideas here.
What is Pydantic?
Pydantic is a Python library designed for data validation and parsing using Python type annotations. While it shares similarities with dataclasses in creating structured data containers, Pydantic additionally provides extensive runtime data validation.
How Pyrefly Supports Pydantic
- Understands Pydantic constructs like
BaseModel,Field,ConfigDict, and model-level config options. - Performs static analysis that mirrors Pydantic's runtime validation logic, minimizing false positives in your IDE.
- Provides immediate feedback (e.g. red squiggles or type errors) when the code would fail under Pydantic's actual behavior.
- Does not require a plugin or manual config — support is builtin and automatic.
Validation Modes
Pydantic models can use two validation modes:
- Lax (Default): Values are automatically coerced when possible. For example, a string like
"123"can be coerced to an integer. - Strict: Coercion is disabled, and only exactly matching types are accepted.
Pyrefly reads your model config to determine the validation mode, so it can strike a balance between providing useful typing and IDE support while maintaining Pydantic's flexibility.
How Lax Mode Works in Pyrefly
In lax mode (the default), Pyrefly uses named union types to represent the acceptable input types for each field. These named unions keep type signatures concise and readable while closely reflecting Pydantic's runtime coercion behavior.
For example, when you define a field with type int in lax mode, Pyrefly represents it as LaxInt, which is equivalent to int | bool | float | str | bytes | Decimal (based on Pydantic's conversion table):
from pydantic import BaseModel
from typing import reveal_type
from decimal import Decimal
class Model(BaseModel):
x: int = 0
reveal_type(Model.__init__) # revealed type is (self: Model, *, x: LaxInt = ..., **Unknown) -> None
# int field accepts: int, bool, float, str, bytes, Decimal
Model(x=1)
Model(x=True)
Model(x=1.0)
Model(x='123')
Model(x=b'123')
Model(x=Decimal('123'))
Note: Pyrefly applies named unions (like
LaxInt) to atomic types and recursively to nested types. Container types are converted to more general types to handle variance: for example,list[int]becomesIterable[LaxInt]. When you use union types (e.g.,int | bool), each member is expanded individually and then flattened into a regular union.
For a complete reference of all type conversions in lax mode, see the Lax Mode Type Conversions page.
Comparison to Existing Tools
Mypy’s Pydantic plugin has five configuration options to control how strict the checking is — for example, whether coercion is allowed or extra fields are permitted.
Pyrefly works differently. It doesn't rely on external config. Instead, it inspects your code directly — things like strict=True or extra='forbid' and strikes a balance between Pydantic's flexibility and Pyrefly's type checking.
How to Use
You don’t need to enable or configure anything to use Pyrefly’s Pydantic support.
Just:
- Install
pydantic(preferably v2). - Install
pyrefly(version 0.33.0 or later). - Write your Pydantic models as usual.
- Run Pyrefly on your code.
Pyrefly will recognize Pydantic constructs like BaseModel, Field, and model_config, and provide appropriate type checking automatically. You can follow this link to try it out on some small examples.
Supported Features with Examples
The following examples showcase which Pydantic features are currently supported by Pyrefly. Pyrefly does not cover all Pydantic features, but these features should provide good coverage of the most common Pydantic use cases. You can request additional Pydantic features to be supported by opening a GitHub issue.
Immutable fields with ConfigDict
from pydantic import BaseModel, ConfigDict
# Marking a model as frozen (immutable)
class Model(BaseModel):
model_config = ConfigDict(frozen=True)
x: int = 42
m = Model()
m.x = 10 # Error: Cannot set field `x` because the model is frozen
Strict vs Non-Strict Field Validation
from pydantic import BaseModel, Field
# Lax mode (default): runtime coercion allowed
class User(BaseModel):
name: str
age: int
# This passes at runtime and in Pyrefly because age accepts strings in lax mode
y = User(name="Alice", age="30")
# Strict mode: enforce exact types, no coercion
class User2(BaseModel):
name: str
age: int = Field(strict=True)
# This triggers type errors in Pyrefly and red squiggles in the IDE,
# and will also fail at runtime due to type mismatch.
z = User2(name="Alice", age="30") # Error: age expects int, not str
Handling Extra Fields in Pydantic Models
By default, Pydantic models allow extra fields (fields not defined in the model) to be passed during initialization. This behavior is consistent with Pyrefly’s support, which follows the default extra='allow' behavior.
from pydantic import BaseModel
# Extra fields allowed by default
class ModelAllow(BaseModel):
x: int
# This works fine: extra field `y` is allowed and ignored
ModelAllow(x=1, y=2)
# Explicitly forbid extra fields by setting `extra='forbid'`
class ModelForbid(BaseModel, extra="forbid"):
x: int
# This will raise a type error because of unexpected field `y`, which is consistent with runtime behavior.
ModelForbid(x=1, y=2)
Handling field constraints
Pyrefly provides limited support for range constraints on fields.
from pydantic import BaseModel, Field
class Model(BaseModel):
x: int = Field(gt=0, lt=10)
Model(x=5) # OK
Model(x=0) # Error: Argument value `Literal[0]` violates Pydantic `gt` constraint for field `x`
Model(x=15) # Error: Argument value `Literal[15]` violates Pydantic `lt` constraint for field `x`
Root Models
from pydantic import RootModel, StrictInt
class IntRootModel(RootModel[int]):
pass
class StrictIntRootModel(RootModel[StrictInt]):
pass
m1 = IntRootModel(123) # OK
m2 = IntRootModel("123") # OK - lax mode allows string-to-int coercion
m3 = StrictIntRootModel("123") # Error: StrictInt doesn't allow coercion
Alias validation
from pydantic import BaseModel, Field
class Model(BaseModel, validate_by_name=True, validate_by_alias=True):
x: int = Field(alias='y')
# both `x` and `y` are valid aliases
Model(x=0)
Model(y=0)