_display()` method
- Types Django's class-based generic views and the mixins that compose them — when a view is parameterized by a model, attributes inherited from the framework are typed against that model
- Provides immediate feedback when the code has type errors
- Does **not** require a plugin or manual config — support is built-in and automatic
---
## Some Supported Features with Examples
The following examples showcase which Django features are currently supported by Pyrefly. This is a subset of Django's full feature set, but covers the most common use cases.
### Auto-Generated Fields
Django automatically adds certain fields to every model, even if you don't define them explicitly.
#### Primary Key: `id` Field
By default, Django automatically adds an `id` field to serve as the primary key (unless you define a custom primary key):
```python
from django.db import models
class Reporter(models.Model):
full_name = models.CharField(max_length=70)
# Django auto-adds: id = models.AutoField(primary_key=True)
reporter = Reporter()
assert_type(reporter.id, int)
```
#### Custom Primary Keys
If you define a field with `primary_key=True`, Django will not add the `id` field. Pyrefly correctly infers the type of custom primary keys:
```python
from django.db import models
class Reporter(models.Model):
uuid = models.UUIDField(primary_key=True)
full_name = models.CharField(max_length=70)
reporter = Reporter()
assert_type(reporter.uuid, UUID)
assert_type(reporter.pk, UUID) # pk aliases the custom primary key
```
#### ForeignKey `_id` Suffix Fields
For every `ForeignKey` field named `X`, Django automatically creates a field named `X_id` that stores the ID of the related object:
```python
class Article(models.Model):
reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)
# Django auto-adds: reporter_id: int
article = Article()
assert_type(article.reporter_id, int)
```
### ForeignKey Relationships
A `ForeignKey` creates a many-to-one relationship where each instance of one model relates to an instance of another model.
```python
from django.db import models
class Reporter(models.Model):
full_name = models.CharField(max_length=70)
class Article(models.Model):
reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)
```
#### Basic Forward Access
Accessing a `ForeignKey` field returns an instance of the related model:
```python
article = Article()
assert_type(article.reporter, Reporter)
```
#### Chained Access
You can access fields on the related model:
```python
assert_type(article.reporter.full_name, str)
```
#### Nullable ForeignKeys
If a `ForeignKey` has `null=True`, Pyrefly reflects this in the inferred type:
```python
class Article(models.Model):
reporter = models.ForeignKey(Reporter, null=True, on_delete=models.CASCADE)
article = Article()
assert_type(article.reporter, Reporter | None)
```
### ManyToManyField Relationships
A `ManyToManyField` creates a many-to-many relationship where instances of one model can be related to multiple instances of another model.
```python
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author, related_name='books')
```
#### Forward Managers
Accessing a `ManyToManyField` returns a manager object that provides methods to interact with the related objects:
```python
book = Book()
assert_type(book.authors, ManyRelatedManager[Author, models.Model])
assert_type(book.authors.all(), QuerySet[Author, Author])
```
The manager provides methods like `.add()`, `.remove()`, `.clear()`, and `.all()` to manage the relationship.
---
### Django Model Enums
Pyrefly supports Django's model choices using `Choices`, `IntegerChoices`, and `TextChoices`. These provide type-safe enumerations for model fields.
```python
from django.db import models
class Vehicle(models.IntegerChoices):
CAR = 1, "Car"
TRUCK = 2, "Truck"
MOTORCYCLE = 3, "Motorcycle"
class Product(models.Model):
vehicle_type = models.IntegerField(choices=Vehicle.choices)
# Pyrefly correctly infers enum types
assert_type(Vehicle.CAR.value, int)
assert_type(Vehicle.CAR.label, str)
assert_type(Vehicle.values, list[int])
assert_type(Vehicle.choices, list[tuple[int, str]])
```
Pyrefly also supports `TextChoices` and the base `Choices` class with various value types including `enum.auto()` for automatic value generation.
---
## factory_boy Support
Pyrefly recognizes [factory_boy](https://factoryboy.readthedocs.io/) factories that subclass `DjangoModelFactory`. For each factory, it figures out which Django model is produced and infers the correct return types for `create`, `build`, `create_batch`, and `build_batch`. No plugin or extra configuration is required.
The examples below all build on this `User` model and `UserFactory`:
```python
# myapp/models.py
from django.db import models
class User(models.Model):
username = models.CharField(max_length=150)
```
```python
# myapp/factories.py
from factory.django import DjangoModelFactory
from myapp.models import User
class UserFactory(DjangoModelFactory):
class Meta:
model = User
```
### `create()` and `build()`
`UserFactory.create()` and `UserFactory.build()` each return a `User` instance:
```python
user = UserFactory.create()
assert_type(user, User)
user = UserFactory.build()
assert_type(user, User)
```
Field access on the returned instance is type-checked against the model:
```python
assert_type(user.username, str)
```
### `create_batch()` and `build_batch()`
`UserFactory.create_batch(n)` and `UserFactory.build_batch(n)` return `list[User]`:
```python
users = UserFactory.create_batch(3)
assert_type(users, list[User])
users = UserFactory.build_batch(3)
assert_type(users, list[User])
```
---
## Differences from Mypy
Mypy uses a plugin (`mypy-django-plugin`) that provides very detailed type information by accessing runtime Django internals and performing multiple passes over the code. Pyrefly takes a different approach by following the type stubs directly without runtime introspection.
### Type Representation Differences
In some cases, such as `ManyToManyField` relationships, Mypy and Pyrefly infer different types:
**Example:**
```python
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author, related_name="books")
class Article(models.Model):
headline = models.CharField(max_length=200)
authors = models.ManyToManyField(Author, related_name="articles")
# What types do the managers have?
book = Book()
article = Article()
```
Mypy (with django plugin):
- `book.authors` has type: `Author_ManyRelatedManager[Book_authors]`
- `article.authors` has type: `Author_ManyRelatedManager[Article_authors]`
- These are **different types** (different class name, different type parameter)
- Mypy will **reject** assigning one to the other
Pyrefly (following stubs):
- `book.authors` has type: `ManyRelatedManager[Author, Model]`
- `article.authors` has type: `ManyRelatedManager[Author, Model]`
- These are the **same type**
- Pyrefly will **accept** assigning one to the other
---
## Features Not Yet Supported
These are some of the Django features that are **not yet supported**:
### Reverse Relationships
Django automatically creates reverse relationships for `ForeignKey` and `ManyToManyField`. For example:
```python
class Reporter(models.Model):
full_name = models.CharField(max_length=70)
class Article(models.Model):
reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE)
# Not yet supported:
reporter = Reporter()
reveal_type(reporter.article_set) # Expected: RelatedManager[Article]
```
### Advanced QuerySet Operations
While basic `.all()` operations are supported, more complex QuerySet operations may not have complete type inference.
Those are not the only unsupported features, so if there are specific features you would like to see, please request them by opening a github issue and adding the Django label to it.
---
---
title: Pyrefly Error Kinds
description: Pyrefly error categories and suppression codes
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Pyrefly Error Kinds
An _error kind_ categorizes an error by the part of the typing specification
that an error is related to. Every error has exactly one kind.
The main use of error kinds is as short names ("slugs") that can be used in
error suppression comments.
Diagnostics have several possible severity levels, which can be [configured](../configuration#errors) in `pyrefly.toml` or via CLI:
- **ignore**: the diagnostic is not emitted
- **info**: the diagnostic shows up blue in the IDE
- **warn**: the diagnostic shows up yellow in the IDE
- **error**: the diagnostic shows up red in the IDE
The default severity for diagnostics is `error` unless otherwise noted. Diagnostics with default severity `ignore` must be explicitly enabled to be emitted.
By default, `pyrefly check` only displays diagnostics with severity `error` or higher.
Use the [`min-severity`](../configuration#min-severity) config option or `--min-severity` CLI flag to also show lower-severity diagnostics (e.g. `--min-severity warn`).
By default, only `error`-level diagnostics cause a nonzero exit code in the CLI. When [`min-severity`](../configuration#min-severity) is set, any diagnostic at or above that threshold causes a nonzero exit.
In the IDE, diagnostics below `error` severity are only shown for files that are currently open in the editor.
## abstract-method-call
This error is raised when code attempts to invoke a method decorated with
`@abstractmethod`. Abstract methods have no implementation, so calling them is
always invalid, even if the signature would otherwise match.
```python
from abc import ABC, abstractmethod
class Base(ABC):
@classmethod
@abstractmethod
def build(cls) -> "Base": ...
Base.build()
# Cannot call abstract method `Base.build` [abstract-method-call]
```
## assert-type
An `assert-type` error is raised when a `typing.assert_type()` call fails.
## bad-argument-count
This error arises when a function is called with the wrong number of arguments.
```python
def takes_three(one: int, two: int, three: int) -> complex:
...
takes_three(1, 2, 3, 4) # Expected 3 positional arguments, got 4 [bad-argument-count]
```
Note that `missing-argument` will be raised if pyrefly can identify that
specific arguments are missing. As such, this error is more likely to appear
when too many args are supplied, rather than too few.
This example shows both kinds of errors:
```python
from typing import Callable
def apply(f: Callable[[int, int], int]) -> int:
return f(1) # Expected 1 more positional argument [bad-argument-count]
apply() # Missing argument `f` in function `apply` [missing-argument]
```
## bad-argument-type
This error indicates that the function was called with an argument of the wrong
type.
```python
def example(x: int) -> None:
...
example("one") # Argument `Literal['two']` is not assignable to parameter `x` with type `int` in function `example` [bad-argument-type]
```
This can also happen with `*args` and `**kwargs`:
```python
def bad_args(*args: int) -> None:
...
bad_args(1, "two") # Argument `Literal['two']` is not assignable to parameter with type `int` in function `bad_args` [bad-argument-type]
```
```python
def bad_kwargs(**kwargs: int) -> None:
...
bad_args(x=1, y="two") # Keyword argument `y` with type `Literal['two']` is not assignable to kwargs type `int` in function `bad_kwargs` [bad-argument-type]
```
## bad-assignment
The most common cause of this error is attempting to assign a value that conflicts with the variable's type annotation.
```python
x: str = 1 # `Literal[1]` is not assignable to `str` [bad-assignment]
```
However, it can occur in several other situations.
Here, `x` is marked as `Final`, so assigning a new value to it is an error.
```python
from typing import Final
x: Final = 1
x = 2 # `x` is marked final [bad-assignment]
```
In another case, attempting to annotate an assignment to an instance attribute raises this error.
```python
class A:
x: int
a = A()
a.x: int = 2 # Type cannot be declared in assignment to non-self attribute `a.x` [bad-assignment]
```
## bad-class-definition
This error indicates that there is something wrong with the class definition.
It tends to be a bit rarer, since most issues would be tagged with other error kinds, such as
`annotation-mismatch` or one of the function errors.
Inheritance has its own complexities, so it has its own error kind called `invalid-inheritance`.
One place you may see it is dynamic class generation:
```python
from enum import Enum
Ex = Enum("Ex", [("Red", 1), ("Blue", 2), ("Red", 3)]) # Duplicate field `Red` [bad-class-definition]
```
However, it is best practice to use the class syntax if possible, which doesn't treat duplicate names as an error.
It also covers decorators applied to a class kind they cannot be applied to,
such as `@dataclass` on a `Protocol`, `@disjoint_base` on a `TypedDict` or
`Protocol`, or `@runtime_checkable` on a non-`Protocol` class.
## bad-context-manager
This error occurs when a type that cannot be used as a context manager appears in a `with` statement.
```python
class A:
def __enter__(self): ...
with A(): ... # `A` is missing an `__exit__` method!
```
## bad-dunder-all
This error occurs when `__all__` is explicitly defined for a module but contains an entry that cannot be found in the module's definitions, wildcard imports, or submodules (for `__init__.py` files).
```python
__all__ = ["x", "y"] # Name `y` is listed in `__all__` but is not defined in the module
x = 5
```
To fix this error, either define the missing name or remove it from `__all__`:
```python
__all__ = ["x", "y"]
x = 5
y = 10 # Now `y` is defined
```
## bad-function-definition
Like `bad-class-definition`, this error kind is uncommon because other error kinds are used for more specific issues.
For example, argument order is enforced by the parser, so `def f(x: int = 1, y: str)` is a `parse-error`.
It also covers decorators applied to a function when the typing spec only
allows them on classes, such as `@disjoint_base` on a function.
## bad-index
Attempting to access a container with an incorrect index.
This only occurs when Pyrefly can statically verify that the index is incorrect, such as with a fixed-length tuple.
```python
def add_three(x: tuple[int, int]) -> int:
return x[0] + x[1] + x[2] # Error: index 2 is out of range.
```
Pyrefly also knows the keys of `TypedDict`s, but those have their own error kind.
## bad-instantiation
This error occurs when attempting to instantiate a class that cannot be instantiated, such as a protocol:
```python
from typing import Protocol
class C(Protocol): ...
C() # bad-instantiation
```
## bad-keyword-argument
bad-keyword-argument pops up when a keyword argument is given multiple values:
```python
def f(x: int) -> None:
pass
f(x=1, x=2)
```
However, this is often accompanied by a `parse-error` for the same issue.
## bad-match
This error is used in two cases.
The first is when there is an issue with a `match` statement. For example, `Ex` only has 2 fields but the `case` lists 3:
```python
class Ex:
__match_args__ = ('a', 'b')
def __init__(self, a: int, b: str) -> None:
self.a = a
self.b = b
def do(x: Ex) -> None:
match x:
case Ex(a, b, c):
print("This is an error")
```
It is also used when `__match_args__` is defined incorrectly. It must be a tuple of the names of the class's attributes as literal strings.
For class `Ex` in the previous example, `__match_args__ = ('a', 'c')` would be an error because `Ex.c` does not exist.
## bad-override
When a subclass overrides a field or method of its base class, care must be taken that the override won't cause problems.
Some of these are obvious:
```python
class Base:
def f(self, a: int) -> None:
pass
class NoArg(Base):
def f(self) -> None:
pass
class WrongType(Base):
def f(self, a: str) -> None:
pass
def uses_f(b: Base) -> None:
b.f(1)
```
These errors are rather obvious: `uses_f` will fail if given a `NoArg` or `WrongType` instance, because those methods don't expect an `int` argument!
The guiding idea here is the [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle), the idea that a subclass can stand in for a base class at any point without breaking the program.
This can be a little subtle at first blush. Consider:
```python
class Base:
def f(self, a: int) -> None:
pass
class Sub(Base):
def f(self, a: float) -> None:
pass
```
Is this OK? Yes! `int` is treated as a subclass of `float`, or to put it another way, a function that accepts `float` can accept every `int`.
That means everywhere that we call `Base.f` can safely call `Sub.f`.
The opposite case, where `Base.f` takes `float` and `Sub.f` takes `int`, is an error because `Sub.f` cannot accept every `float` value.
Note that bad overrides caused by inconsistent parameter names are separately reported as [bad-override-param-name](#bad-override-param-name), and bad overrides caused by mutable attribute type changes are separately reported as [bad-override-mutable-attribute](#bad-override-mutable-attribute).
## bad-override-mutable-attribute
Arises when a subclass overrides a mutable (read-write) attribute of a parent class with an incompatible type.
Mutable attributes require invariant types — the child's type must be exactly compatible with the parent's — because the attribute can be both read and written through a reference to the parent class.
```python
class Base:
x: int | str
class Sub(Base):
x: int # Error: narrows the type
```
This is unsafe because code that holds a `Base` reference can write any `int | str` value to `x`, but `Sub` only expects `int`:
```python
def f(b: Base) -> None:
b.x = "hello" # valid for Base, but breaks Sub's invariant
f(Sub())
```
This is a sub-kind of [bad-override](#bad-override): suppressing `bad-override` also suppresses this error.
Other type checkers like mypy do not enforce this check by default, so this error kind can be selectively disabled if desired.
## bad-override-param-name
Arises when a subclass overrides a method of its base class while changing the name of a positional parameter.
This is a sub-kind of [bad-override](#bad-override): suppressing `bad-override` also suppresses this error.
Changing the name of a parameter breaks callers that pass in an argument by name:
```python
class Base:
def f(self, a: int) -> None:
pass
class Sub(Base):
def f(self, b: int) -> None:
pass
def f(base: Base):
base.f(a=0)
f(Sub()) # oops!
```
## bad-param-name-override
Deprecated: this error code has been renamed to [bad-override-param-name](#bad-override-param-name).
The old name is still accepted in suppression comments and configuration for backwards compatibility.
## bad-raise
In a `raise` statement of the form `raise x from y`, `x` must be an exception, and `y` must be an exception or `None`.
```python
def bad_raise() -> None:
raise Exception() # ok
raise 1 # error
raise Exception() from None # ok
raise Exception() from Exception() # ok
raise Exception() from 1 # error
```
## bad-return
Arises when a function does not return a value that is compatible with the function's return type annotation.
```python
def bad_return() -> None:
return 1
```
Real-world examples are often less obvious, of course, due to complex control flow and type relationships.
This error is also raised for generator functions:
```python
from typing import Generator
# Generator has 3 types: the yield, send, and return types.
def bad_gen() -> Generator[int, None, str]:
yield 1
return 2 # should be a str!
```
## bad-specialization
"Specialization" refers to instantiating a generic type with a concrete type.
For example, `list` is a generic type, and `list[int]` is that type specialized with `int`.
Each generic type has an expected number of type vars, and each type var can be bound or constrained.
Attempting to use specialize a generic type in a way that violates these specifications will result in a `bad-specialization` error:
```python
x: list[int, str] # Error: expected 1 type argument, got 2.
class A[T: str]: ...
y: A[int] # Error: `int` is not assignable to `str`.
```
## bad-typed-dict
This error is reported when a `TypedDict` definition includes an unsupported keyword argument.
```python
from typing import TypedDict
# This is an error because `foo` is not a valid keyword.
class InvalidTD(TypedDict, foo=1):
x: int
# This is valid.
class ValidTD(TypedDict, total=False):
x: int
```
## bad-typed-dict-key
This error arises when `TypedDict`s are used with incorrect keys, such as a key that does not exist in the `TypedDict`.
```python
from typing import TypedDict
class Ex(TypedDict):
a: int
b: str
def test(x: Ex) -> None:
# These two keys don't exist
x.nope
x["wrong"]
# TypedDict keys must be strings!
x[1]
```
## bad-unpacking
An error caused by unpacking, such as attempting to unpack a list, tuple, or iterable into the wrong number of variables.
```python
def two_elems() -> tuple[int, str]:
return (1, "two")
a, b, c = two_elems()
```
Note that pyrefly can only report this error if it knows how many elements the thing being unpacked has.
```python
# A bare `tuple` could have any number of elements
def two_elems() -> tuple:
return (1, "two")
a, b, c = two_elems()
```
## deprecated
Default severity: `warn`
This warning occurs on usage of a deprecated class or function.
```python
from warnings import deprecated
@deprecated("deprecated")
def f(): ...
f() # deprecated!
```
## division-by-zero
Default severity: `warn`
Division, floor division, or modulo by a literal zero value. This catches cases where the divisor is the literal `0` or a variable with type `Literal[0]`, which would always raise `ZeroDivisionError` at runtime.
```python
x = 10 / 0 # error: division by zero
y = 10 // 0 # error: division by zero
z = 10 % 0 # error: division by zero
```
## explicit-any
Default severity: `ignore`
`typing.Any` was written explicitly in an annotation. This diagnostic is useful for codebases that want to prevent `Any` from silently allowing arbitrary operations.
```python
from typing import Any
def f(x: Any) -> Any: # explicit-any on both annotations
return x
# Fix:
def f(x: object) -> object:
return x
```
## implicit-abstract-class
Default severity: `ignore`
Pyrefly emits this error when a class that inherits from an abstract class but is not itself explicitly abstract (for example, it does not directly inherit from `abc.ABC` or use `abc.ABCMeta`) has unimplemented abstract members. Such classes cannot be instantiated at runtime. To resolve the issue, explicitly declare the class as abstract or provide concrete implementations.
```python
from abc import ABC, abstractmethod
class A(ABC):
@abstractmethod
def f(self) -> int: ...
class B(A): ... # Error: `B` cannot be instantiated due to unimplemented abstract method `f`.
```
Two possible fixes:
```python
# 1. If `B` is not meant to be instantiable, explicitly declare it as abstract.
class B(A, ABC): ...
```
```python
# 2. Or, implement `f` to make `B` instantiable.
class B(A):
def f(self) -> int:
return 0
```
## implicit-any
Default severity: `ignore`
Umbrella error code for cases where Pyrefly infers an implicit `Any`. Suppressing or enabling `implicit-any` cascades to every sub-kind below — useful when you want to surface (or silence) all implicit-`Any` cases without listing them individually.
Most concrete diagnostics are emitted under one of the more specific sub-kinds:
- [`implicit-any-attribute`](#implicit-any-attribute)
- [`implicit-any-empty-container`](#implicit-any-empty-container)
- [`implicit-any-parameter`](#implicit-any-parameter)
- [`implicit-any-type-argument`](#implicit-any-type-argument)
## implicit-any-attribute
Default severity: `ignore`
An attribute is implicitly inferred to be `Any | None` (or `tuple[Any, ...]`) because it was assigned `None` or `()` in a method without an explicit type annotation. Add an annotation so the attribute's type is determined at the assignment site rather than leaking `Any` into downstream uses.
This is a sub-kind of [implicit-any](#implicit-any): suppressing `implicit-any` also suppresses this error.
```python
class C:
def __init__(self):
self.a = None # implicit-any-attribute (type: None | Any)
self.b = () # implicit-any-attribute (type: tuple[Any, ...])
# Fix:
class C:
a: int | None
b: tuple[int, ...]
def __init__(self):
self.a = None
self.b = ()
```
## implicit-any-empty-container
Default severity: `ignore`
An empty container literal (`[]`, `{}`) couldn't be inferred from context and was pinned to a container of `Any`. Provide a type annotation, initialize with a non-empty value, or use the value in a way that lets Pyrefly infer its element type.
This is a sub-kind of [implicit-any](#implicit-any): suppressing `implicit-any` also suppresses this error.
```python
x = [] # implicit-any-empty-container — type: list[Any]
# Fix:
x: list[int] = []
# or
x = [1, 2, 3]
```
## implicit-any-parameter
Default severity: `ignore`
A function parameter has no type annotation, so Pyrefly treats it as `Any`. Add an annotation. The `self` and `cls` parameters of methods are excluded from this check.
This is a sub-kind of [implicit-any](#implicit-any): suppressing `implicit-any` also suppresses this error.
```python
def f(x): # implicit-any-parameter — `x` has type `Any`
return x + 1
# Fix:
def f(x: int) -> int:
return x + 1
```
## implicit-any-type-argument
Default severity: `ignore`
A generic class, type alias, or special form (`tuple`, `Callable`, `type`) was used without explicit type arguments, so Pyrefly defaulted the missing type parameters to `Any`. Provide explicit arguments, or declare a default for the relevant type variable.
This is a sub-kind of [implicit-any](#implicit-any): suppressing `implicit-any` also suppresses this error.
```python
def f(xs: list) -> tuple: # implicit-any-type-argument on both `list` and `tuple`
return tuple(xs)
# Fix:
def f(xs: list[int]) -> tuple[int, ...]:
return tuple(xs)
```
## implicit-import
Default severity: `warn`
This error is emitted when a submodule is accessed through a parent package without being explicitly imported in the current file.
While Python’s global module cache (`sys.modules`) might allow this to work if another part of your program performed the import earlier, relying on this side effect is fragile. If that external code is refactored or removed, your code will crash at runtime with an `AttributeError`.
To fix this, always use an explicit import for the submodule you want to access.
```python
import urllib
# error: 'urllib' has no attribute 'request' (unless imported elsewhere)
urllib.request.urlopen(...)
# Fix:
import urllib.request
urllib.request.urlopen(...)
```
## implicitly-defined-attribute
Default severity: `ignore`
An attribute was implicitly defined by assignment to `self` in a method that we
do not recognize as always executing. We recognize constructors and some test
setup methods; we will emit an error for any attributes defined by assignment
in other methods.
```python
class C:
def __init__(self):
self.x = 0 # no error, `__init__` always executes
def f(self):
self.y = 0 # error, `y` may be undefined if `f` does not execute
```
## incompatible-comparison
Default severity: `ignore`
This error is raised when Pyrefly can prove that an equality (`==`) or inequality
(`!=`) comparison is made between incompatible built-in types (for example, `int`
versus `str`). This check is currently limited to a small allowlist: numeric
types (including `decimal.Decimal`), bytes-like types, set-like types, and `str`.
It does not analyze unions, `None`, or user-defined classes.
```python
x: int = 1
y: str = ""
if x == y: ... # Comparison `==` between incompatible types `int` and `str` [incompatible-comparison]
```
## incompatible-overload-residual
This error is raised when we match an overloaded function against a type containing a type variable and cannot find a consistent type for that variable.
This usually indicates a higher-order call where each overload branch implies incompatible constraints for the same type variable.
```python
from typing import Callable, overload
@overload
def f(x: int) -> float: ...
@overload
def f(x: str) -> str: ...
def f(x): ...
def project[S](func: Callable[[S], S], y: S) -> Callable[[], S]:
return lambda: y
project(f, 1) # Overload type was not compatible with solved type variables: S = int
```
## inconsistent-inheritance
When a class inherits from multiple base classes, the inherited fields must be consistent.
Example:
```python
class A:
f: str
class B:
f: int
class C(A, B): ... # error, the field `f` is inconsistent
```
## inconsistent-overload
The signature of a function overload is inconsistent with the implementation.
See the [typing specification](https://typing.python.org/en/latest/spec/overload.html#implementation-consistency)
for details on the consistency checks Pyrefly performs.
Example:
```python
from typing import overload
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ... # error, overload accepts `str` but implementation only accepts `int`
def f(x: int) -> int | str:
return x
```
## inconsistent-overload-default
In an overloaded function, the type of a parameter in an overload signature is inconsistent with
its default value in the implementation.
Example:
```python
from typing import Literal, overload
@overload
def f(x: Literal[True] = ...) -> None: ... # error, `x` has default `False`, which is inconsistent with type `Literal[True]`
@overload
def f(x: Literal[False]) -> int: ...
def f(x: bool = False) -> int | None:
return 0 if x else None
```
## internal-error
Ideally you'll never see this one. If you do, please consider [filing a bug](https://github.com/facebook/pyrefly/issues).
## invalid-annotation
There are several reasons why an annotation may be invalid. The most common case is misusing a typing special form, such as `typing.Final`, `typing.ClassVar`, `typing.ParamSpec`, and so on.
Even when no configuration file is present, this diagnostic is shown as a warning in the IDE.
```python
from typing import *
# Final must have a value
a: Final
# ClassVar can only be used in a class body
b: ClassVar[int] = 1
```
The error messages will explain how the special form is being misused. Consult the [typing docs](https://docs.python.org/3/library/typing.html) and [typing spec](https://typing.python.org/en/latest/spec/) for more information.
## invalid-argument
This error is used to indicate an issue with an argument to special typing-related functions.
For example, `typing.NewType` is a handy special form for creating types that are distinct from a base type.
```python
from typing import *
# Invalid argument to `NewType`: the first arg must match the name.
Mismatch = NewType("Wrong Name", int)
# Invalid argument to `isinstance`: `NewType`s cannot be used in `isinstance`.
UserId = NewType("UserId", int)
if isinstance(1, UserId):
...
```
## invalid-decorator
Default severity: `warn`
This error indicates that a method-only decorator (`@final`, `@override`, etc.) was applied to a top-level function. Such usage is harmless at runtime and is sometimes intentional, so the default severity is `warn`.
```python
from typing import final
@final
def f() -> None:
pass
```
Decorator misuse that violates the typing spec — for example, `@dataclass` on a
`Protocol`, `@disjoint_base` on a `TypedDict` or `Protocol`,
`@runtime_checkable` on a non-`Protocol` class, or `@disjoint_base` on a
function — is reported under `bad-class-definition` or
`bad-function-definition` instead, both of which default to `error`.
## invalid-inheritance
An error caused by incorrect inheritance in a class or type definition.
This can pop up in quite a few cases:
- Trying to subclass something that isn't a class.
- Subclassing a type that does not support it, such as a `NewType` or a `Final` class.
- Attempting to mix `Protocol`s with non-`Protocol` base classes.
- Trying to make a generic enum.
- Trying to give a `TypedDict` a metaclass.
And so on!
## invalid-literal
`typing.Literal` only allows a [limited set](https://typing.python.org/en/latest/spec/literal.html#legal-parameters-for-literal-at-type-check-time) of types as parameters.
Attempting to use `Literal` with anything else is an error.
```python
from typing import Literal
# These are legal
Literal[1]
Literal['a', 'b', 'c']
# This is not
class A:
...
Literal[A()]
```
## invalid-overload
The `@overload` decorator requires that the decorated function has at least two overloaded signatures and a base implementation.
```python
from typing import *
@overload
def no_base(x: int) -> None:
pass
@overload
def no_base(x: str) -> int:
pass
```
```python
@overload
def just_one(x: int) -> None:
pass
def just_one(x: str) -> None:
...
```
## invalid-param-spec
This error is reported when `typing.ParamSpec` is defined incorrectly or misused. For example:
```python
from typing import *
P = ParamSpec("Name Must Match!")
P1 = ParamSpec("P1")
P2 = ParamSpec("P2")
def f(x, *args: P1.args, **kwargs: P2.kwargs) -> None:
pass
```
Here, `P1.args` and `P2.kwargs` can't be used together; `*args` and `**kwargs` must come from the same `ParamSpec`.
## invalid-pattern
This error is reported when a pattern is invalid at runtime. For example, enum members are values,
so they must be matched as value patterns (without `()`), not class patterns:
```python
from enum import Enum
class Color(Enum):
RED = "red"
def describe(color: Color) -> None:
match color:
case Color.RED(): # Invalid pattern: use `Color.RED` (without parentheses)
pass
```
## invalid-self-type
This error occurs when `Self` is used in a context Pyrefly does not currently support.
For example, Pyrefly does not currently allow `Self` for `TypedDict`, so the
following code would error:
```python
from typing import *
class TD(TypedDict):
x: Option[Self]
```
## invalid-sentinel
An error caused by incorrect definition of a Sentinel. A few examples:
```python
from typing_extensions import Sentinel
# First argument passed to sentinel constructor isn't a string literal
my_str: str = "MISSING"
A = Sentinel(my_str)
# Invalid arguments passed to sentinel constructor
MISSING = Sentinel("MISSING", non_existent="")
```
## invalid-super-call
`super()` has [a few restrictions](https://docs.python.org/3/library/functions.html#super) on how it is called.
`super()` can be called without arguments, but only when used inside a method of a class:
```python
class Legal(Base1, Base2):
def f(self) -> None:
super().f()
def illegal(arg: SomeType) -> None:
super().f()
```
When the function is called with two arguments, like `super(T, x)`, then `T` must be a type, and the second argument is either an object where `isinstance(x, T)` is true
or a type where `issubclass(x, T)` is true.
## invalid-syntax
This error covers syntactical edge cases that are not flagged by the parser.
For example:
```python
x: list[int] = [0, 2, 3]
x[0]: int = 1
```
It's not a parse error for an assignment to have an annotation, but it is forbidden by the type checker to annotate assignment to a subscript like `x[0]`.
## invalid-type-alias
An error related to the definition or usage of a `typing.TypeAlias`. Many of these cases are covered by [`invalid-annotation`](#invalid-annotation), so this error
specifically handles illegal type alias values:
```python
from typing import TypeAlias
x = 2
Bad: TypeAlias = x
```
## invalid-type-var
An error caused by incorrect usage or definition of a TypeVar. A few examples:
```python
from typing import TypeVar
# Old-style TypeVars must be assigned to a matching variable.
Wrong = TypeVar("Name")
# PEP 695-style TypeVars can be constrained, but there must be at least two:
def only_one_constraint[T: (int,)](x: T) -> T:
...
# It's also illegal to mix the two styles together.
T = TypeVar("T")
def mixed[S](a: S, b: T) -> None:
...
```
## invalid-type-var-tuple
An error caused by incorrect usage or definition of a TypeVarTuple.
TypeVarTuple has similar error cases to [TypeVar](#invalid-type-var), but also a few of its own. For example:
```python
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")
# TypeVarTuples must always be unpacked:
bad: tuple[Ts] = (...)
good: tuple[*Ts] = (...)
# Only one TypeVarTuple is allowed in a list of type arguments:
def two_tups[*Xs, *Ys](xs: tuple[*Xs], ys: tuple[*Ys]) -> None:
...
```
## invalid-variance
An error caused by a type variable being used in a position incompatible with its declared variance.
For example, a covariant type variable cannot be used in a contravariant position (such as a method parameter), and a contravariant type variable cannot be used in a covariant position (such as a return type).
```python
from typing import TypeVar, Generic
T_co = TypeVar("T_co", covariant=True)
T_contra = TypeVar("T_contra", contravariant=True)
class BadCovariant(Generic[T_co]):
# Error: covariant type variable used in contravariant position
def set_value(self, value: T_co) -> None: ...
class BadContravariant(Generic[T_contra]):
# Error: contravariant type variable used in covariant position
def get_value(self) -> T_contra: ...
```
## invalid-yield
This error arises when `yield` is used in a way that is not allowed. For example:
```python
from typing import Generator
for _ in range(1, 10):
yield "can't yield outside of a function!"
def bad_yield_from() -> Generator[int, None, None]:
# `yield from` can only be used with iterables.
yield from 1
```
## missing-argument
An error caused by calling a function without all the required arguments.
```python
def takes_two(x: int, y: int) -> int:
return x + y
takes_two(1)
```
## missing-attribute
This error is raised when attempting to access an attribute that does not exist on the given object or module.
In the case of modules, attempting to import an nonexistent name will raise [`missing-module-attribute](#missing-module-attribute) instead.
```python
import os
from os import bacarat # missing-module-attribute
os.jongleur() # missing-attribute
```
Note that objects with type `Any` will never raise this error.
## missing-import
A module could not be found.
The error message will include which paths were searched, such as the site package paths.
You may be missing a dependency, or you may need to inform Pyrefly where the module lives. See [Configuration](configuration.mdx) for further information.
Even when no configuration file is present, this diagnostic is shown as a warning in the IDE.
## missing-module-attribute
Arises when attempting to import a name that does not exist from a module.
This is distinct from [`missing-import`](#missing-import), which is used when the module being imported does not exist, and [`missing-attribute`](#missing-attribute), when access attributes of the module.
```python
import this_does_not_exist # missing-import
import os.bacarat # missing-import
from os import joker # missing-module-attribute
os.perkeo # missing-attribute
```
In this example, `os.bacarat` is treated as a module name, so failing to find it results in an `missing-import`.
`from os import joker` does not tell us if `joker` is a module, class, function, etc., so it is treated as the more general `missing-module-attribute`.
## missing-override-decorator
Default severity: `ignore`
A method overrides a parent class method but does not have the `@override` decorator. We do not emit this error for dunder methods that are inherited from `object` (e.g., `__repr__`, `__eq__`, `__str__`), but we do emit it for dunder methods inherited from other classes.
This error supports strict override enforcement as specified in the [typing spec](https://typing.python.org/en/latest/spec/class-compat.html#strict-enforcement-per-project). When enabled, it requires all overriding methods to be explicitly marked with `@typing.override`.
```python
from typing import override
class Base:
def foo(self) -> None: ...
def __len__(self) -> int: ...
class Derived(Base):
def foo(self) -> None: ... # missing-override-decorator
def __len__(self) -> int: ... # missing-override-decorator (inherited from Base, not object)
def __repr__(self) -> str: ... # OK (inherited from object)
@override
def foo(self) -> None: ... # OK
```
To enable strict override enforcement, set the severity to `error` in your configuration:
```toml
[tool.pyrefly]
errors = { missing-override-decorator = "error" }
```
## missing-source
Default severity: `ignore`
Pyrefly was able to find a stubs package but no corresponding source package. For example, this can
happen if you install the `types-requests` package but forget to install `requests`.
## missing-source-for-stubs
Pyrefly has bundled stubs for a package, but no corresponding source package was found.
## name-mismatch
Default severity: `warn`
This warning indicates that the first string argument to a functional type definition does not
match the name it is assigned to.
```python
from collections import namedtuple
from enum import Enum
RepoDetails = namedtuple("repo_details", ["source_dir", "age"])
DviState = Enum("_dvistate", "pre outer inpage")
```
## no-access
The `no-access` error indicates that an attribute exists, but it cannot be used in this way.
For example, classes do not have access to their instances' attributes:
```python
class Ex:
def __init__(self) -> None:
self.meaning: int = 42
del Ex.meaning # no-access
```
## no-matching-overload
This error is similar to the other bad function call errors, but specifically for cases where a function decorated with `@overload` is called with arguments that do not match any of the overloaded variations.
For example, neither of the signatures of `f` can take an argument of type `float`:
```python
from typing import overload
@overload
def f(x: int) -> int:
...
@overload
def f(x: str) -> str:
...
def f(x: int | str) -> int | str:
return x
f(1.0)
```
## non-convergent-recursion
Default severity: `warn`
Some Python code has type analysis that is recursive. Consider, for example:
```
def f(): return g()
def g(): return [f()]
```
Here, return-type inference for `f` depends on the return type of `g`, and vice versa
so we get a cycle. Similarly, in
```
x = 1
while some_condition():
x = [x]
```
the type of `x` at the end of the loop body depends on the type of `x` at the beginning,
and this forms a cycle (and in this case the type doesn't converge to any closed form;
the true type is a recursive type but Pyrefly will not infer an anonymous recursive type).
Many more cycles are possible, sometimes involving harder-to-visualize problems like
classes whose type parameters have constraints that are recursive, or class fields
whose inferred types depend on one another.
Pyrefly will attempt to resolve all such recursion using a *fixpoint*, where we
repeatedly analyze the related entities in type inference. But because the results
do not always converge (like `x = [x]` above), we have to limit the number of
iterations, which means sometimes we cannot ensure that the inferred result
is correct.
In these cases, Pyrefly will produce a `non-convergent-recursion` error that
warns you our fixpoint did not converge, and tells you the result we inferred
based on the last iteration. In some cases, the error message will recommend
that by adding more annotations you may be able to help Pyrefly determine the
correct type.
If you're filing a bug report for this error, set the environment variable
`PYREFLY_FIXPOINT_DETAILS=1` when running Pyrefly and include additional
diagnostic information in the issue that will help the Pyrefly team root cause
the non-convergence.
## non-exhaustive-match
Default severity: `warn`
Pyrefly warns when a `match` statement over an `Enum` attempts to enumerate the members
but forgets at least one case. Add the missing members or a default arm.
```python
from enum import Enum
class Color(Enum):
RED = "red"
BLUE = "blue"
def describe(color: Color) -> str:
match color: # non-exhaustive-match
case Color.RED:
return "danger"
```
## not-a-type
This indicates an attempt to use something that isn't a type where a type is expected.
In most cases, a more specific error kind is used.
You may see this error around incorrect type aliases:
```python
class A:
...
# Not an alias, just a string!
X = "A"
x: X = ... # X is not a type alias, so this is illegal
```
## not-async
`not-async` is reported when attempting to `await` on something that is not
awaitable. This may indicate that a function should have been marked `async` but
wasn't.
```python
def some_func() -> None:
...
await some_func() # Expression is not awaitable [not-async]
```
This will also arise if the context manager used in an `async with` statement
has `__aenter__` and `__aexit__` methods that are not marked `async`.
The fix is to use an `async` function in the `await`. This may mean making the
function `async` or finding an existing `async` function to use instead.
## not-callable
A straightforward error: something that is not a function was used as if it were a function.
One interesting place this error may occur is with decorators:
```python
x = 1
@x # not-callable
def foo() -> None:
...
```
## not-iterable
This is most likely to be seen in a `for` loop:
```python
x = 1 # Or some other value
for val in x: # not-iterable
...
```
## not-required-key-access
Default severity: `ignore`
This warning indicates that a [`TypedDict`](https://typing.python.org/en/latest/spec/typeddict.html)
key marked `NotRequired` (or inherited from a `total=False` TypedDict) is being accessed without
first ensuring that the key is present. Even if the value type is non-optional, the key itself may
not exist at runtime, so Pyrefly encourages guarding the access with an `in` check or a `.get()`
call.
```python
from typing import NotRequired, TypedDict
class Movie(TypedDict):
title: str
year: NotRequired[int]
def describe(movie: Movie) -> int:
return movie["year"] # not-required-key-access
def safe_describe(movie: Movie) -> int:
if "year" in movie:
return movie["year"] # OK: key presence established
raise ValueError("Missing year")
```
## open-unpacking
Default severity: `ignore`
This error is reported on an attempt to unpack an
[open](https://typing.python.org/en/latest/spec/glossary.html#term-open) TypedDict that potentially
has items incompatible with the TypedDict it is being unpacked into.
Example:
```python
from typing import TypedDict
class OpenTypedDict(TypedDict):
x: int
class UnpackingTarget(TypedDict):
x: int
y: str
def f(o: OpenTypedDict) -> UnpackingTarget:
# Error: `o` could be an instance of a subclass of `OpenTypedDict` with an
# item `y` with an incompatible type.
return {"y": "", **o}
```
To fix this error, close the open TypedDict to indicate it does not contain any unknown items:
```python
class OpenTypedDict(TypedDict, closed=True): ...
```
Note: In Python versions before 3.15, import `TypedDict` from `typing_extensions` rather than
`typing` to use the `closed` feature.
## parse-error
An error related to parsing or syntax. This covers a variety of cases, such as function calls with duplicate keyword args, some poorly defined functions, and so on.
## potential-bad-keyword-argument
A potential conflict between an explicit keyword argument and a NotRequired TypedDict field. The field may be absent at runtime, so the conflict is not guaranteed. This is a separate error code from `bad-keyword-argument` to allow users to opt-in to this stricter check.
```python
from typing import TypedDict, NotRequired
class Options(TypedDict, total=False):
name: str
def f(name: str, **kwargs) -> None: ...
opts: Options = {}
# Potential conflict: if 'name' is in opts at runtime, this will crash
f(name="test", **opts) # E: Multiple values for argument `name`
```
## protocol-implicitly-defined-attribute
Protocols must declare the attributes they require directly in the class body. Assigning to a new `self` attribute inside a protocol method introduces a member that implementations of the protocol would never be required to provide.
Add an annotated attribute (or property) to the protocol, or remove the assignment.
```python
from typing import Protocol
class Template(Protocol):
name: str
def method(self) -> None:
self.temp: list[int] = [] # protocol-implicitly-defined-attribute
```
## pytorch-efficiency-lint-cuda-call
Default severity: `ignore`
Calling `.cuda()` on a `torch.Tensor` hard-codes the target device to CUDA. Use `.to(device)` instead so your code works on any accelerator (CUDA, XPU, MPS, etc.).
Enable this lint with [`pytorch-efficiency-lints = true`](./configuration.mdx#pytorch-efficiency-lints) in your `pyrefly.toml`.
```python
import torch
def f(x: torch.Tensor) -> None:
y = x.cuda() # pytorch-efficiency-lint-cuda-call
# Fix: y = x.to(device)
```
## pytorch-efficiency-lint-item-call
Default severity: `ignore`
Calling `.item()` on a `torch.Tensor` forces GPU-to-CPU synchronization, blocking the training loop until all pending GPU operations complete. This can reduce GPU utilization from over 90% to under 50%. Prefer `tensor[0]` for scalar tensors, accumulate values on the GPU with `torch.sum()`, or defer `.item()` to outside the training loop.
Enable this lint with [`pytorch-efficiency-lints = true`](./configuration.mdx#pytorch-efficiency-lints) in your `pyrefly.toml`.
```python
import torch
def f(x: torch.Tensor) -> None:
v = x.item() # pytorch-efficiency-lint-item-call
```
## pytorch-efficiency-lint-print-tensor
Default severity: `ignore`
Passing a `torch.Tensor` to `print()` triggers `Tensor.__repr__()`, which forces GPU-to-CPU synchronization and blocks until all pending GPU operations complete. Use `print(tensor.shape)` to inspect metadata without synchronizing, or guard with `if DEBUG: print(tensor)`.
Enable this lint with [`pytorch-efficiency-lints = true`](./configuration.mdx#pytorch-efficiency-lints) in your `pyrefly.toml`.
```python
import torch
def f(x: torch.Tensor) -> None:
print(x) # pytorch-efficiency-lint-print-tensor
# Fix: print(x.shape) or print(x.dtype)
```
## pytorch-efficiency-lint-redundant-to-call
Default severity: `ignore`
Calling `.to(device)` on a tensor returned by a factory function like `torch.zeros()` allocates the tensor on CPU first, then copies it to the target device. Pass `device=` directly to the factory function instead to avoid the redundant allocation and copy.
Enable this lint with [`pytorch-efficiency-lints = true`](./configuration.mdx#pytorch-efficiency-lints) in your `pyrefly.toml`.
```python
import torch
device = torch.device("cuda")
x = torch.zeros(3, 4).to(device) # pytorch-efficiency-lint-redundant-to-call
# Fix: x = torch.zeros(3, 4, device=device)
```
## read-only
This error indicates that the attribute being accessed does exist but cannot be modified.
For example, a `@property` with no setter cannot be assigned to:
```python
class Ex:
@property
def meaning(self) -> int:
return 42
x = Ex()
x.meaning = 0
```
## redefinition
Pyrefly reports this error when a name that already has an annotation in the current scope is annotated again with a different type. Re-annotating the same variable can lead to confusing types; prefer introducing a new name instead.
```python
def f(x: int) -> None:
x: str = str(x) # redefinition
```
## redundant-cast
Default severity: `warn`
This warning is raised when `typing.cast()` is used to cast a value to a type it is already compatible with. Such casts are unnecessary and can be removed to improve code clarity.
```python
import typing
x: int = 42
# This cast is redundant since x is already an int
y = typing.cast(int, x) # redundant-cast
# This is a valid cast since we're casting from a more general type
obj: object = "hello"
s = typing.cast(str, obj) # No warning - this is a valid cast
```
The redundant cast warning helps identify unnecessary type casts that don't provide any additional type safety benefits.
## redundant-condition
Default severity: `warn`
This error is used to indicate a type that's equivalent to True or False is used as a boolean condition (e.g. an uncalled function)
```python
def f() -> bool:
...
# This is likely a mistake, as it's likely that the function needs to be invoked.
if f:
...
# This is likely a mistake, as it's equivalent to `if True`.
if "abc":
...
```
## reveal-type
Default severity: `info`
Pyrefly uses this diagnostic to communicate the output of the [`reveal_type`](https://typing.python.org/en/latest/spec/directives.html#reveal-type) function.
`reveal_type` is a *directive* — it is always shown in CLI output regardless of the [`min-severity`](../configuration#min-severity) threshold, and is never subject to suppression or baseline exclusion. To hide it, set `reveal-type = "ignore"` in the [`errors`](../configuration#errors) table.
## string-as-iterable
Default severity: `ignore`
This warning is raised when a string is passed to a parameter expecting an `Iterable[str]` or
`Sequence[str]`. While `str` is technically iterable, it iterates by individual characters, which
is often not what you intended.
```python
from typing import Iterable
def takes_items(xs: Iterable[str]) -> None:
...
takes_items("hello") # Passing `str` treats it as an iterable of characters
```
## unannotated-attribute
Default severity: `ignore`
Deprecated: this error code has been renamed to [implicit-any-attribute](#implicit-any-attribute). The old name is still accepted in suppression comments and configuration for backwards compatibility.
## unannotated-parameter
Default severity: `ignore`
Deprecated: this error code has been renamed to [implicit-any-parameter](#implicit-any-parameter). The old name is still accepted in suppression comments and configuration for backwards compatibility.
## unannotated-protocol-member
This error is raised when a protocol member is assigned a value in the class body without an explicit type annotation. Protocol members must have explicitly declared types so that implementations know exactly what type to provide.
```python
from typing import Protocol
class MyProto(Protocol):
x = None # error: Protocol member `x` must have an explicit type annotation
# Fixed version:
class MyProto(Protocol):
x: int | None = None
```
## unannotated-return
Default severity: `ignore`
This error is raised when a function is missing a return type annotation. This helps enforce fully-typed codebases by ensuring all functions declare their return types explicitly. To fix it, add a return type annotation to the function.
```python
def calculate_sum(x: int, y: int): # error: `calculate_sum` is missing a return annotation
return x + y
# Fixed version:
def calculate_sum(x: int, y: int) -> int:
return x + y
```
## unbound-name
Pyrefly found a conditional definition for the given name, so it may be undefined in some flows. For example:
```python
def f(check: bool):
if check:
x = 1
return x # unbound-name
```
Compare this with [unknown-name](#unknown-name), which is reported when no definition at all is found for a name.
## unexpected-keyword
A function was called with an extra keyword argument.
```python
def two_args(a: int, b: int) -> int:
...
two_args(a=1, b=2, c=3)
```
## unexpected-positional-argument
A positional argument was passed for a keyword-only parameter.
```python
def takes_kwonly(*, x: int) -> int:
...
takes_kwonly(1) # should be `takes_kwonly(x=1)`!
```
## unimported-directive
Using a [type checker directive](https://typing.python.org/en/latest/spec/directives.html#type-checker-directives) without importing it from `typing` first will result in a runtime error.
```python
reveal_type(1) # error
assert_type(1, int) # error
```
## unknown-name
A name is referenced but does not exist.
Compare this with [unbound-name](#unbound-name), which is reported when the name is conditionally defined.
Even when no configuration file is present, this diagnostic is shown as a warning in the IDE.
```python
def where() -> None:
# There is no spoon: unknown-name
global spoon
```
## unnecessary-comparison
Default severity: `warn`
This warning is raised when an identity comparison (`is` or `is not`) is made between
literals whose comparison result is statically known.
```python
def test0() -> None:
# Different literals are always different objects
if 1 is 2: # unnecessary-comparison: always False
pass
# Same singletons are always the same object
if True is not False: # unnecessary-comparison: always True
pass
class User: ...
class Admin(User): ...
def test1(user: User) -> None:
# Comparing an instance to a class is always False
if user is Admin: # unnecessary-comparison: did you mean isinstance(user, Admin)?
pass
```
This check is relatively conservative and only warns on limited cases where the comparison is highly likely to be redundant.
## unnecessary-type-conversion
Default severity: `warn`
This warning is raised when a builtin type constructor (`str`, `int`, `float`, `bool`, or `bytes`) is called on a value that is already of that type, making the conversion redundant.
```python
def f(x: str) -> None:
y = str(x) # unnecessary-type-conversion: `x` is already of type `str`
```
## unreachable
Default severity: `warn`
This error is raised when a `return` or `yield` can never be reached because it comes
after a statement that always exits the current flow, such as `return`, `raise`, `break`, or `continue`.
```python
def example():
return 1
return 2 # This `return` statement is unreachable [unreachable]
def generator():
return
yield 1 # This `yield` expression is unreachable [unreachable]
def loop_example():
while True:
break
return 1 # This `return` statement is unreachable [unreachable]
```
Note that `yield` statements can follow other `yield` statements without error, since generators
can produce multiple values:
```python
def valid_generator():
yield 1
yield 2 # This is valid
```
## unreachable-match-case
Default severity: `warn`
:::info
This is a new error kind in *upcoming* Pyrefly version 1.1.0. It is not yet available in the current stable version.
:::
This warning is raised when a `case` pattern in a `match` statement can never match
the subject because the subject's type is disjoint from the pattern's type.
```python
def example(x: list[int]) -> None:
match x:
case 1: # Case pattern can never match subject of type `list[int]` [unreachable-match-case]
pass
```
This check currently covers value patterns (literals, `None`) and class patterns
on attributes, but does not flag top-level class patterns like `case SomeClass()`.
## unresolvable-dunder-all
Default severity: `warn`
This warning is raised when `__all__` is defined but its value cannot be
statically analyzed. This can happen when `__all__` is assigned a function call,
a variable, a list comprehension, or any expression that Pyrefly cannot resolve
at analysis time.
When this occurs, Pyrefly falls back to inferring public exports from
module-level definitions (all names that do not start with an underscore).
```python
def generate_all():
return ["x", "y"]
__all__ = generate_all()
# `__all__` could not be statically analyzed [unresolvable-dunder-all]
```
## unsafe-overlap
Protocols decorated with `@runtime_checkable` may be used in `isinstance` and `issubclass` checks, but the runtime will only checks that all the required attributes are present, without looking at their types.
This error occurs when the object you're checking against the protocol has all the required attributes, but their types are not compatible.
In the example below, `C` should not match with `P`, but the `isinstance` check will succeed at runtime.
```python
from typing import Protocol, runtime_checkable
@runtime_checkable
class P(Protocol):
x: int
class C:
x: str
c = C()
if isinstance(c, P):
pass
```
## unsupported
This error indicates that pyrefly does not currently support a typing feature.
## unsupported-delete
This error occurs when attempting to `del` something that cannot be deleted.
Besides obvious things like built-in values (you can't `del True`!), some object attributes are protected from deletion.
For example, read-only and required `TypedDict` fields cannot be deleted.
## unsupported-operation
This error arises when attempting to perform an operation between values of two incompatible types.
```python
if "hello" in 1: # int doesn't support `in`!
...
```
## untyped-import
Default severity: `warn`
Type information for some third-party libraries is shipped in a stubs package separate from the
library's source code. This error is emitted when we detect that a library is being used without
the recommended stubs package being installed.
## unused-coroutine
If the result of an async function call is not awaited or used, we will raise an error.
```python
async def foo():
return 1
async def bar():
foo() # error
await foo() # ok
x = foo() # ok
```
## unused-ignore
Default severity: `ignore`
This error is raised when a `# pyrefly: ignore` comment is not used to suppress an error, and can be safely removed.
## variance-mismatch
Default severity: `warn`
The inferred variance of a type variable does not match its declared variance. This warning is raised for protocols where the way a type variable is used implies a different variance than what was declared. For example, if a protocol only uses `T` in covariant positions but `T` is declared as invariant, this warning suggests declaring `T` as covariant.
```python
from typing import Protocol, TypeVar
T = TypeVar("T")
class A(Protocol[T]): # variance-mismatch: Type variable `T` in class `A` is declared as invariant, but could be covariant based on its usage
def f(self) -> T: ...
```
---
---
title: Pyrefly Error Suppressions
description: Learn how to suppress type check errors in Pyrefly with code comments and baseline files.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
import CodeSnippet from '@site/src/sandbox/CodeSnippet'
## Error Suppression Comments
The Python type system allows you to suppress errors with a comment. This feature can be helpful in many scenarios. For example, after enabling a type checker, suppressions can allow you to get a clean type check signal without having to stop and fix every pre-existing error in your code.
There are multiple ways to do this in Pyrefly.
```python
def foo() -> int:
# pyrefly: ignore
return "this is a type error"
```
You can also put the comment on the same line as the error.
```python
def foo() -> int:
return "this is a type error" # pyrefly: ignore
```
You can also target specific error types:
```python
def foo() -> int:
return "this is a type error" # pyrefly: ignore[bad-return]
```
We respect the specification and allow `type: ignore` to be used:
```python
def foo() -> int:
return "this is a type error" # type: ignore
```
We also have a special comment that will ignore all errors in a file.
int:
# this error won't be reported
return ""
`}
/>
Pyrefly can automatically suppress all type errors in your project by running:
```
pyrefly suppress
```
This is equivalent to `pyrefly check --suppress-errors`.
By default, `pyrefly suppress` places suppression comments on the line before the error. If you use other tools that also add comments on the line before (e.g. linters, other type checkers), their suppression comments may conflict with each other. To reduce conflicts, you can use `--comment-location=same-line` to place Pyrefly's suppression comments as trailing comments on the same line as the error:
```
pyrefly suppress --comment-location=same-line
```
## Baseline Files (Experimental)
Pyrefly also supports storing errors in a baseline file. Any errors matching the baseline will be ignored and only new errors will be reported.
This is useful when introducing type checking to a project for the first time, or when rolling out changes that require many suppression comments.
This feature is inspired by tools like [basedpyright](https://docs.basedpyright.com/latest/benefits-over-pyright/baseline/) and [Android Studio](https://developer.android.com/studio/write/lint#snapshot).
To generate (or re-generate) the baseline file:
```
pyrefly check --baseline="" --update-baseline
```
To check your project using a baseline file and report only newly-introduced errors, you can either use the CLI flag:
```
pyrefly check --baseline=""
```
Or specify the baseline in your configuration file (`pyrefly.toml` or `pyproject.toml`):
```toml
# pyrefly.toml
baseline = "baseline.json"
```
```toml
# pyproject.toml
[tool.pyrefly]
baseline = "baseline.json"
```
When the baseline is specified in the configuration file, you don't need to pass the `--baseline` flag on every invocation. The CLI flag takes precedence if both are specified.
Note that `baseline` is a **project-level setting** and cannot be overridden in [`sub-config`](./configuration.mdx#sub-configs) sections. If you need different baseline files for different parts of your codebase, consider using separate Pyrefly configuration files.
Errors are matched with the baseline by looking at file, error code, and column number.
Note that errors suppressed by the baseline file are still shown in the IDE.
This feature is experimental, so please submit any feedback or requests you have on our Github repo.
## Upgrading Pyrefly (And other changes that introduce new type errors)
Upgrading the version of Pyrefly you're using, or a third party library you depend on can surface new type errors in your code. Fixing them all at once is often not realistic. We've written scripts to help you temporarily silence them.
```
# step 1
pyrefly suppress
```
```
# step 2
```
```
# step 3
pyrefly suppress --remove-unused
```
Repeat the steps above until you get a clean formatting run and a clean type check.
This will add ` # pyrefly: ignore` comments to your code that will enable you to silence errors, and come back and fix them at a later date. This can make the process of upgrading a large codebase much more manageable.
:::tip
If your project uses other tools that place suppression comments on the line before the error (e.g. other type checkers or linters), use `pyrefly suppress --comment-location=same-line` in step 1 to avoid conflicts.
:::
:::note
`pyrefly suppress` is equivalent to `pyrefly check --suppress-errors`, and `pyrefly suppress --remove-unused` is equivalent to `pyrefly check --remove-unused-ignores`.
:::
---
---
title: Import Resolution
slug: /import-resolution
description: How imports in a given file are found by Pyrefly and their bindings are resolved, including files that are being type checked
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Import Resolution
This doc describes how imports in a given file are found within Pyrefly while performing
a type check or resolving IDE language support operations.
NOTE: see the [Configuration documentation](./configuration.mdx) for more info on
the config options referenced below.
## Relative Imports
If the import is relative (starting with one or more dots), the import is
resolved relative to the path of the file importing it. A single dot at the
beginning of the import (e.g. `.file.to.import`) represents the current
directory, and more dots (e.g. `..other.file`) will continue to walk upward.
## Absolute Imports
For absolute imports, Pyrefly searches for a match in each of the following groups. The
matching process is explained in the next paragraph.
1. Try to import from the search path. See the [search path section](#search-path) for more information.
2. Try to import from `typeshed`.
3. Try to import from the fallback search path. See the [fallback search path section](#fallback-search-path) for
more information on the contents of the search path.
4. Try to import from the site package path. See the
[site package path section](#site-package-path) for more information on the contents
of the site package path.
5. Return an import error.
When searching for a match in one of the above groups, Pyrefly performs the following process
over two passes, one looking for stub *packages*, and the other looking for source *packages*. See
[Stub Files vs Source Files](#stub-files-vs-source-files) for more information.
1. Attempt to match each part of the name to directories in the group, selecting the first
match that is found.
2. If the result is a `.pyi` file or regular package (directory with an
`__init__.py`/`__init__.pyi` file), return the result. Otherwise, keep searching and
attempt to find a `.pyi` file or regular package.
### Search Path
The search path ([see `search-path` in configuration docs](configuration.mdx#search-path))
consists of several entries representing project files.
1. Search path from CLI args.
2. Search path from config files.
3. If [`disable-search-path-heuristics`](configuration.mdx#disable-search-path-heuristics)
is not set, Pyrefly appends an import root directory to the search path.
The import root is:
1. `src/` if there's a `src/` directory in the same directory as the config file.
2. The parent directory (`..`) if there's an `__init__.py` or `__init__.pyi` in the same
directory as the config file.
3. Otherwise, the directory containing the config file.
### Fallback Search Path
The fallback search path is a heuristic automatically constructed by Pyrefly to attempt to
find project files when there's no config file marking the project root, and Pyrefly
is unable to determine from other heuristics where an import root might be.
It is only constructed when
[`disable-search-path-heuristics`](configuration.mdx#disable-search-path-heuristics)
is not set.
The fallback search path consists of each directory from the directory containing
a given file to the root of your filesystem. For example, if you have the following setup:
```
/
|- projects/
|- project_a/
| |- b/
| | |- c.py
| |- d.py
|- project_e/
|- f.py
```
`c.py`'s fallback search path would be `['/projects/project_a/b', '/projects/project_a',
'/projects', '/']`
- `d.py` could be importable with the paths `d`, `project_a.d`, or `projects.project_a.d`.
- `f.py` could be importable with the paths `project_e.f` or `projects.project_e.f`.
`e.py`'s fallback search path would be `['/projects/project_a', '/projects', '/']`
- `c.py` could be importable with the paths `b.c`, `project_a.b.c`, or `projects.project_a.b.c`
- `f.py` could be importable with the paths `project_e.f` or `projects.project_e.f`
`f.py`'s fallback search path would be `['/projects/project_e', '/projects', '/']`
- `c.py` could be importable with the paths `project_a.b.c` or `projects.project_a.b.c`
- `d.py` could be importable with the paths `project_a.d` or `projects.project_a.d`
### Site Package Path
The site package path
([see `site-package-path` in configuration docs](configuration.mdx#site-package-path))
consists of several entries representing third-party packages.
1. Site package path from a config file (if no CLI override is present) or CLI args.
2. A site package path queried from a Python interpreter, if one could be found.
See [Environment Autoconfiguration](configuration.mdx#environment-autoconfiguration)
for more information on finding interpreters.
## Stub Files vs Source Files
A
[stub file](https://typing.python.org/en/latest/spec/distributing.html#stub-files)
is any file that ends with a `.pyi` file suffix. They have many uses, including
adding typing to non-Python extension code, distributing typing information
separate from implementation, or overriding an implementation with more accurate
typing information.
A stub package is a second package corresponding to a regular package, with `-stubs`
appended to its name. A `-stubs` package should only include stub files (`.pyi`),
which override any `.py` or `.pyi` files in the non-stubs package. These are preferred
when available, since they contain the interfaces a library exposes to developers. An
example of this includes the popular library [`pandas`](https://github.com/pandas-dev/pandas),
and its stub package, [`pandas-stubs`](https://github.com/pandas-dev/pandas-stubs).
When importing from a non-stubs package, Pyrefly loads typing information from
imports by first searching for a relevant `-stubs` package, then by looking at
the non-stubs package's `.pyi` files, then falls back to a `.py` file. See
[Absolute Imports](#absolute-imports) for details on when non-stubs packages
are allowed to be used for types, and how you can override that behavior.
## Bundled Third Party Stubs
Pyrefly bundles stubs for several popular third party libraries along with its binary which are
used for type checking and IDE features. These bundled stubs have special import resolution rules which differ
from how installed stubs might be treated. The decision tree below describes the scenarios in which third party stubs
are used.
```
- Pyrefly config file present?
- Yes
- Is the package installed?
- Yes
- Are stubs installed by user?
- Yes
Installed stubs are used.
- No
UntypedImport error.
- No
MissingImport error.
- No
- Is the package installed?
- Yes
Bundled stubs used.
- No
MissingSourceForStubs error.
```
## Editable Installs
When using static analysis tools with an editable install, the editable install should be configured to use `.pth`
files that contain file paths (`/project/src/module`) rather than executable lines (started with `import`) that
install import hooks. See [setuptools doc](https://setuptools.pypa.io/en/latest/userguide/development_mode.html)
and [PEP 660](https://peps.python.org/pep-0660/) for more information.
Import hooks can provide an editable installation that offers a more accurate representation of the actual installation
environment. However, since resolving module locations through an import hook
**requires executing Python code at runtime**, they are incompatible with Pyrefly and other static analysis tools that
operate without code execution. Consequently, when an editable install is configured to use import hooks, Pyrefly
will be unable to automatically locate and analyze the corresponding source files, resulting in incomplete type checking and
code analysis.
Setuptools build system uses import hooks by default for editable installations. To ensure compatibility between
setuptools-based editable installs and Pyrefly, setuptools must be configured to use path-based `.pth` files instead.
This configuration should be performed through the build frontend (such as `pip`) by specifying the appropriate
options during installation or in the project's configuration files.
### uv with setuptools
When using [uv](https://docs.astral.sh/uv/) with setuptools, uv can be
[configured](https://docs.astral.sh/uv/reference/settings/#config-settings) to avoid import hooks.
NOTE: The `uv_build` backend always uses path-based `.pth` files.
### pip with setuptools
When using `pip` with setuptools-based projects, there are two ways to avoid import hooks:
[compat mode](https://setuptools.pypa.io/en/latest/userguide/development_mode.html#legacy-behavior)
and [strict mode](https://setuptools.pypa.io/en/latest/userguide/development_mode.html#strict-editable-installs).
### Hatch / Hatchling
[Hatchling](https://hatch.pypa.io/1.9/config/build/) uses path-based `.pth` files by default.
It will only use import hooks if you set [`dev-mode-exact` to true](https://hatch.pypa.io/latest/config/build/#dev-mode).
### PDM
[PDM](https://backend.pdm-project.org/) uses path-based `.pth` files by default.
It will only use import hooks if you set
[`editable-backend` to "editables"](https://backend.pdm-project.org/build_config/#choose-the-editable-build-format).
### Poetry / Poetry-core
[Poetry-core](https://github.com/python-poetry/poetry-core) backend always uses path-based `.pth` files.
## Debugging Import Issues
Pyrefly has a `dump-config` command that dumps the import-related config options it is using for
each file it is checking. To use it, simply replace `check` with `dump-config` in your
command-line invocation.
---
---
title: Introduction
description: Guides and references for all you need to know about Pyrefly type checker and IDE extension.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
import CodeSnippet from '@site/src/sandbox/CodeSnippet'
Pyrefly is a fast type checker and language server for Python with powerful IDE features. It analyzes your Python code to help you catch type-related errors before your code runs, making your applications more reliable and easier to maintain. Pyrefly supports both IDE integration and CLI usage, giving you flexibility in how you incorporate type checking into your workflow.
## Benefits of Type Checking
Adding type annotations to your Python code and using a type checker like Pyrefly provides several important benefits:
- **Catch bugs early** - Identify type-related errors during development rather than at runtime
- **Improve code quality** - Type annotations serve as living documentation, making your code more readable and self-documenting
- **Enhance developer experience** - Get better IDE support with accurate autocomplete, refactoring tools, and inline documentation
- **Safer refactoring** - Make large-scale changes with confidence, knowing the type checker will catch incompatible type usage
- **Better collaboration** - Types create clear contracts between different parts of your codebase, making it easier for teams to work together
## Try Pyrefly
Here's a simple example showing how Pyrefly can catch type errors:
str:
return "Hello, " + name
# This works fine since both "World" is a string and greet expects a string
message = greet("World")
# Pyrefly catches this error before runtime due to a type misatch between 42 and "str"
# Error: Argument of type 'int' is not assignable to parameter of type 'str'
error_message = greet(42)
`}
/>
In this example, Pyrefly flags the second call to `greet()` because we're passing an integer (`42`) where a string is expected, helping you catch this issue before your code runs. To learn more about Python typing and how to use it effectively:
- If you're new to Python, check out our [Python Typing for Beginners](python-typing-for-beginners) guide.
- If you're familiar with Python but new to typing, see our [Typing for Python Developers](typing-for-python-developers) guide.
---
---
title: Installation
description: How to install Pyrefly
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
Pyrefly is available on [PyPI](https://pypi.org/project/pyrefly/) with a new release every Monday. We often release more frequently when shipping new features and bug fixes.
## What you get out of the box
When you first install Pyrefly without a configuration file, it uses the **basic preset** by default. This shows only high-confidence errors that are very likely to indicate real bugs or crashes, like syntax errors and missing imports. Other diagnostics are silenced.
The basic preset is useful immediately, without setup. You don't need to understand Python typing concepts to benefit. Open a file in VS Code with the Pyrefly extension and you'll get inlay hints, go-to-definition, and the curated error set right away. The status bar shows `Pyrefly (Basic)` so you know which preset is active.
If you already have a `mypy.ini`, `pyrightconfig.json`, or a `[tool.mypy]` / `[tool.pyright]` section in `pyproject.toml`, Pyrefly automatically migrates that configuration the first time it runs in your project. The status bar will show `Pyrefly (Legacy)` (mypy migration) or `Pyrefly (Default)` (pyright migration) accordingly. Nothing is written to disk during the migration; running `pyrefly init` later will commit the same migration permanently.
To unlock full type checking and pin your configuration, run `pyrefly init`. This writes a `pyrefly.toml` (or updates `pyproject.toml`) and explicitly migrates any existing mypy/pyright settings. After running `pyrefly init`, common knobs to tune include:
- `check-unannotated-defs`: type-check the bodies of unannotated functions.
- `errors`: per-error-kind severity overrides.
- `infer-return-types`: when to infer return types for unannotated functions.
- `project-excludes` and `pyrefly suppress`: incremental rollout on existing
codebases.
See the [configuration reference](../configuration) for the full list.
## Install
You can use `uv`, `poetry`, `pip`, `pixi` or `conda` to install Pyrefly. The following commands show you how to install Pyrefly and run 2 basic commands: `init` and `check`.
* `pyrefly init` will update your `pyproject.toml` file (or create a `pyrefly.toml` file) in your project directory, including some basic configuration. It will also attempt to [migrate](../migrating-to-pyrefly) your existing type checker configuration.
* `pyrefly check --summarize-errors` will run the Pyrefly type checker on your project, providing a list of type errors and a summary of error types. The `--summarize-errors` flag is optional, remove it if you don't want summary stats.
* `pyrefly suppress` will mark all existing errors as ignored, allowing you to start with a clean check. (You can also use `pyrefly check --suppress-errors`.)
Simply `cd` into your project directory and run:
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```
pip install pyrefly
pyrefly init
pyrefly check --summarize-errors
```
```
conda install -c conda-forge pyrefly
pyrefly init
pyrefly check --summarize-errors
```
```
uvx pyrefly init
uvx pyrefly check --summarize-errors
```
```
poetry add --group dev pyrefly
poetry run pyrefly init
poetry run pyrefly check --summarize-errors
```
```
pixi add pyrefly
pixi run pyrefly init
pixi run pyrefly check --summarize-errors
```
## Configure
You can set up a basic configuration file to type-check your project. You can add configuration options to a `pyproject.toml` file or create a `pyrefly.toml` file in your project directory. All [configuration options are documented here](../configuration).
```
[tool.pyrefly]
search_path = [
"example_directory/..."
]
```
Then, run `pyrefly check` again, and the tool will use your configuration options.
The tool may return a list of type errors; this is perfectly normal. You have a few options at this point:
1. Use `# pyrefly: ignore` comments to silence the errors. This will get your project to a clean type-checking state, and you can reduce the number of errors as you go. We've included a command that can do this for you:
```
pyrefly suppress
```
2. Use extra configuration options to silence specific categories of errors or exclude files with more errors than average.
----
## Upgrading Pyrefly
Upgrading the version of Pyrefly you're using or a third-party library you depend on can reveal new type errors in your code. Fixing them all at once is often unrealistic. We've written scripts to help you temporarily silence them.
```
# Step 1
pyrefly suppress
```
```
# Step 2
```
```
# Step 3
pyrefly suppress --remove-unused
```
Repeat these steps until you achieve a clean formatting run and a clean type check.
This will add `# pyrefly: ignore` comments to your code, enabling you to silence errors and return to fix them later. This can make the process of upgrading a large codebase much more manageable.
----
## Add Pyrefly to CI
After your project passes type checks without errors, you can prevent new bugs from being introduced. Enforce this through CI (Continuous Integration) to prevent other maintainers from merging code with errors.
### Using the GitHub Action (Recommended)
The simplest way to add Pyrefly to GitHub Actions is with the official composite action. It handles Python setup, installation, and running the type checker with inline PR annotations enabled by default.
```yaml
- uses: facebook/pyrefly@main
```
Here is a complete workflow example:
```yaml
name: Pyrefly Type Check
on:
pull_request:
branches: [main]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: facebook/pyrefly@main
```
You can pin a specific Pyrefly version, set the Python version, or pass extra arguments:
```yaml
- uses: facebook/pyrefly@main
with:
version: "0.60.0"
python-version: "3.12"
args: "--summarize-errors"
```
The action accepts the following inputs:
| Input | Default | Description |
|-------|---------|-------------|
| `version` | latest | Pyrefly version to install (e.g., `0.60.0`) |
| `args` | | Extra arguments passed to `pyrefly check` |
| `python-version` | `3.x` | Python version for `actions/setup-python` |
| `working-directory` | `.` | Directory to run the type check in |
Type errors appear as inline annotations on pull requests automatically via `--output-format=github`.
### Manual Setup
If you need more control or use a non-GitHub CI system (GitLab CI, Jenkins, etc.), you can set up Pyrefly manually.
Save your workflow in the following path within your repository:
```
.github/workflows/typecheck.yml
```
GitHub automatically detects `.yml` files within `.github/workflows/` and sets up the defined workflows.
```yaml
name: Pyrefly Type Check
on:
pull_request:
branches: [main]
workflow_dispatch: # Allows manual triggering from the GitHub UI
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
# Install Python dependencies and create environment
- name: Install dependencies
run: |
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
# Install your dependencies; adjust the following lines as needed
pip install -r requirements-dev.txt
- name: Install Pyrefly
run: pip install pyrefly
- name: Run Pyrefly Type Checker
run: pyrefly check --output-format=github
```
### Inline PR Annotations
Pyrefly can emit errors as [GitHub Actions workflow commands](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions) so that type errors appear inline when reviewing pull requests.
To enable this, pass `--output-format=github`:
```
pyrefly check --output-format=github
```
The GitHub Action enables this by default. For manual setups, add the flag to your workflow step.
### A few notes about this setup:
- Building your environment and installing dependencies will enhance type safety by checking the types of imports. *This is not required, but encouraged!*
- Simply drop in `pyrefly check` to existing workflows that build and test your environment.
```
- name: Run Pyrefly Type Checker
run: pyrefly check
```
- Your `pyrefly.toml` or Pyrefly configs in your `pyproject.toml` will be automatically detected. Learn how to [configure Pyrefly here](../configuration).
----
## Pre-commit
Pyrefly provides a [pre-commit hook](https://pre-commit.com/) so you can automatically type check files before they are committed.
We maintain a dedicated repository for this integration here: [facebook/pyrefly-pre-commit](https://github.com/facebook/pyrefly-pre-commit)
That repository contains:
- A pre-commit hook
- Installation instructions
- Example configuration snippets for your project and CI
To get started, follow the setup steps in the repo’s [README](https://github.com/facebook/pyrefly-pre-commit#readme).
---
---
title: Migrating from Mypy
description: How to switch your type checker configuration from Mypy to Pyrefly
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
## Running Pyrefly
Like mypy, pyrefly can be given a list of files to check:
```sh
$ pyrefly check file1.py file2.py
```
The easiest way to run pyrefly on all files in a project is to run it from the project root:
```sh
$ cd your/project
$ pyrefly check
```
Pyrefly is designed to have sensible defaults, and you may not need to configure it at all.
However, projects with existing mypy configs may want to configure pyrefly to suit their own needs.
## Mypy Config Migration
To make it as easy as possible to get started with pyrefly, we've provided a script for automatically migrating a mypy config to pyrefly.
```sh
$ pyrefly init path/to/your/project
```
This will search for an existing `mypy.ini` or `pyproject.toml` with a `tool.mypy` section, and then transform it into a `pyrefly.toml` (or `[tool.pyrefly]` section) while preserving as many options as possible. See `init --help` for more options.
We do recommend checking the resulting config for errors. While there is some overlap between mypy's config options and pyrefly's config options, it's not always possible to cleanly translate one config option to another.
If you'd rather start fresh with a hand-written config, please see the [pyrefly configuration docs](configuration.mdx).
If you run into any issues with config migration, please [let us know](https://github.com/facebook/pyrefly/issues)!
### Config options
The following config options make Pyrefly behave more like Mypy:
```toml
# By default, mypy does not check unannotated function bodies. The following flags will configure Pyrefly to match mypy's behavior:
check-unannotated-defs = false
infer-return-types = "never"
```
```toml
# If mypy is configured with `--check-untyped-defs` or `--strict` it will check function bodies where the function signature is unannotated.
# The following flags will enable this behavior while still treating unannotated returns as Any (like mypy does):
check-unannotated-defs = true
infer-return-types = "never"
```
```toml
# Direct Pyrefly to respect `# mypy: ignore` and `# mypy: ignore-errors` comments
permissive-ignores = true
```
### Config Migration Details
`files`, `modules`, and `packages` are combined into `project_includes`. This should work exactly the same for `files` and `packages`. Mypy doesn't recurse into `modules`, but pyrefly will.
Pyrefly makes an effort to transform the `exclude` regex into a list of filepath globs for `project_excludes`. This should excel on simple regexes, such as `some/file.py|exclude_dir/`, which becomes `["**/some/file.py", "**/exclude_dir/"]`.
The `ignore_missing_imports` per-module config option is turned into a list of modules. For example:
```ini
[mypy-some.*.module]
ignore_missing_imports = True
```
Becomes:
```toml
replace_imports_with_any = ["some.*.module"]
```
Mypy's `follow_imports = "skip"` is handled the same way.
Pyrefly does support mypy's [module name pattern syntax](https://mypy.readthedocs.io/en/stable/config_file.html#config-file-format): see [Module Globbing](configuration.mdx#module-globbing) in the configuration docs.
Mypy's `follow_untyped_imports` option is allowed to be global or per-module. The pyrefly equivalent, `use_untyped_imports`, is only global. This setting defaults to `true` unless
the `follow_untyped_imports` is disabled in the `[mypy]` section of the migrated config.
### Mypy Error Codes and Pyrefly Error Kinds
Pyrefly maps Mypy's [error codes](https://mypy.readthedocs.io/en/stable/error_code_list.html) to equivalent pyrefly [error kinds](error-kinds.mdx).
While not every error code has an equivalent error kind, we make an effort to ensure that pyrefly suppresses the same errors that mypy does.
This may lead to overly broad error suppressions, and you may want to consider removing some error kinds from the disable list.
You can also use a [SubConfig](configuration.mdx#sub_config) to selectively silence errors in specific files,
or see [Silencing Errors](#silencing-errors) for how to suppress errors at the source.
See [Error Kind Mapping](#error-kind-mapping) for a table showing the relationship between type check diagnostic settings and error kinds.
### Per-Module configs
Mypy's per-module configs let you change a wide range of configuration options for modules matching a module wildcard.
Pyrefly's [SubConfigs](configuration.mdx#sub_config) are a similar mechanism that let you configure pyrefly's behavior for files matching a filepath glob.
However, they support significantly fewer options, and only `disable_error_code` and `enable_error_code` will be migrated over to the pyrefly config.
## Silencing Errors
Like mypy, pyrefly has ways to silence specific error codes. Full details can be found in the [Error Suppression docs](error-suppressions.mdx)
To silence an error on a specific line, add a disable comment above that line. You can either suppress all errors on that line:
```
# pyrefly: ignore
x: str = 1
```
Or target a specific error type:
```
# pyrefly: ignore[bad-assignment]
x: str = 1
```
To suppress all instances of an error, disable that error in the config:
```
[errors]
missing-import = false
```
This is equivalent to mypy's `disable_error_code`, though of course the [error codes](error-kinds.mdx) are different!
### Error Kind Mapping
This table shows the mapping between mypy's [error codes](https://mypy.readthedocs.io/en/stable/error_code_list.html) and pyrefly's [error kinds](error-kinds.mdx).
| Mypy | Pyrefly |
| ------- | ------- |
| abstract | bad-instantiation |
| arg-type | bad-argument-type |
| assert-type | assert-type |
| assignment | bad-assignment |
| attr-defined | missing-attribute |
| await-not-async | not-async |
| call-arg | bad-argument-count |
| call-overload | no-matching-overload |
| deprecated | deprecated |
| dict-item | bad-typed-dict |
| explicit-any | explicit-any |
| import | missing-import |
| import-not-found | missing-import |
| import-untyped | untyped-import |
| index | bad-index, unsupported-operation |
| metaclass | invalid-inheritance |
| name-defined | unknown-name |
| name-match | name-mismatch |
| no-overload-impl | invalid-overload |
| no-untyped-def | implicit-any |
| operator | unsupported-operation |
| override | bad-override |
| possibly-undefined | unbound-name |
| redundant-cast | redundant-cast |
| redundant-expr | redundant-condition |
| return | bad-return |
| return-value | bad-return |
| syntax | parse-error |
| top-level-await | not-async |
| truthy-bool | redundant-condition |
| truthy-function | redundant-condition |
| truthy-iterable | redundant-condition |
| type-arg | implicit-any |
| type-var | bad-specialization |
| typeddict-readonly-mutated | read-only |
| typeddict-unknown-key | bad-typed-dict-key |
| union-attr | missing-attribute |
| unused-awaitable | unused-coroutine |
| unused-coroutine | unused-coroutine |
| used-before-def | unbound-name |
| valid-type | invalid-annotation |
---
---
title: Migrating from Pyright
description: How to switch your type checker configuration from Pyright to Pyrefly
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
## Running Pyrefly
Like pyright, pyrefly can be given a list of files to check:
```sh
$ pyrefly check file1.py file2.py
```
The easiest way to run pyrefly on all files in a project is to run it from the project root:
```sh
$ cd your/project
$ pyrefly check
```
Pyrefly doesn't need a config file to start checking your code. Its sensible defaults are designed to work well for most projects.
However, projects with existing pyright configs may want to configure pyrefly to suit their own needs.
## Pyright Config Migration
To make it as easy as possible to get started with pyrefly, we've provided a script for automatically migrating a pyright config to pyrefly.
```sh
$ pyrefly init path/to/your/project
```
This will search for an existing `pyrightconfig.json` or `pyproject.toml` with a `tool.pyright` section, and then transform it into a `pyrefly.toml` (or `[tool.pyrefly]` section) while preserving as many options as possible. See `init --help` for more options.
There is a significant overlap between pyright's and pyrefly's configuration options, so migration is pretty straightforward. However, it may be worth checking the generated config for errors, just in case.
If you'd rather start fresh with a hand-written config, please see the [pyrefly configuration docs](configuration.mdx).
If you run into any issues with config migration, please [let us know](https://github.com/facebook/pyrefly/issues)!
## Config Migration Details
When it comes to listing files, pyright uses just paths, while pyrefly supports glob patterns. Thankfully, paths are a subset of glob patterns, so pyrefly can just use the paths as-is. You could consider manually simplifying the paths into glob patterns, but it's not necessary.
Pyright supports four platforms: Windows, Linux, Darwin (macOS), and All. Since pyrefly only supports Python's [supported platforms](https://docs.python.org/3/library/sys.html#sys.platform), we choose to treat "All" as "linux".
### Type Check Diagnostic Settings and Error Kinds
Pyrefly maps pyright's [type check diagnostics settings](https://microsoft.github.io/pyright/#/configuration?id=type-check-diagnostics-settings) to equivalent pyrefly [error kinds](error-kinds.mdx).
While not every diagnostic setting has an equivalent error kind, we make an effort to ensure that pyrefly suppresses the same errors that pyright does.
This may lead to overly broad error suppressions, and you may want to consider removing some error kinds from the disable list.
You can also use a [SubConfig](configuration.mdx#sub_config) to selectively silence errors in specific files,
or see [Silencing Errors](#silencing-errors) for how to suppress errors at the source.
See [Error Kind Mapping](#error-kind-mapping) for a table showing the relationship between type check diagnostic settings and error kinds.
### Execution Environments
Pyright's [execution environments](https://microsoft.github.io/pyright/#/configuration?id=execution-environment-options) let you customize the Python version, platform, module search paths, and diagnostic settings for some part of your project.
Pyrefly's [SubConfigs](configuration.mdx#sub_config) are a similar mechanism that let you configure pyrefly's behavior for files matching a filepath glob.
However, subconfigs do not support changing the Python version, platform, or module search paths.
Diagnostic settings are carried over to the equivalent subconfig, using the mapping mentioned [above](#type-check-diagnostic-settings-and-error-kinds).
### Extending Builtins
Pyright automatically imports any builtins defined in `__builtins__.pyi` at the project root or in a custom stubs directory specified by `stubPath` (defaulting to `./typings`).
Pyrefly supports this behavior - the directory for `stubPath` should be added to your Pyrefly config's `site-package-path`.
## Silencing Errors
Like pyright, pyrefly has ways to silence specific error codes. Full details can be found in the [Error Suppression docs](error-suppressions.mdx).
To silence an error on a specific line, add a disable comment above that line. You can either suppress all errors on that line:
```
# pyrefly: ignore
x: str = 1
```
Or target a specific error type:
```
# pyrefly: ignore[bad-assignment]
x: str = 1
```
To suppress all instances of an error, disable that error in the config:
```
[errors]
missing-import = false
```
This is similar to pyright's [type check rule overrides](https://microsoft.github.io/pyright/#/configuration?id=type-check-rule-overrides), though of course the [error codes](error-kinds.mdx) are different!
You can also use:
```toml
permissive-ignores = true
```
To allow `pyright: ignore` comments to be used by Pyrefly.
## Error Kind Mapping
This table shows the mapping between pyright's [type check diagnostics settings](https://microsoft.github.io/pyright/#/configuration?id=type-check-diagnostics-settings)
and pyrefly's [error kinds](error-kinds.mdx).
:::info
When no configuration file is present, Pyrefly shows the following diagnostics as **warnings** in the IDE, matching pyright's default behavior:
- `reportInvalidTypeForm` → [`invalid-annotation`](error-kinds.mdx#invalid-annotation)
- `reportMissingImports` → [`missing-import`](error-kinds.mdx#missing-import)
- `reportUndefinedVariable` → [`unknown-name`](error-kinds.mdx#unknown-name)
This means users migrating from pyright will see familiar diagnostics even before creating a Pyrefly config file.
:::
| Pyright | Pyrefly |
| ------- | ------- |
| reportAbstractUsage | bad-instantiation |
| reportArgumentType | bad-argument-type |
| reportAssertTypeFailure | assert-type |
| reportAssignmentType | bad-assignment |
| reportAttributeAccessIssue | missing-attribute |
| reportDeprecated | deprecated |
| reportExplicitAny | explicit-any |
| reportIncompatibleMethodOverride | bad-override |
| reportIncompatibleVariableOverride | bad-override |
| reportInconsistentOverload | inconsistent-overload |
| reportIndexIssue | bad-index |
| reportInvalidTypeArguments | bad-specialization |
| reportInvalidTypeForm | invalid-annotation |
| reportInvalidTypeVarUse | invalid-type-var |
| reportMissingImports | missing-import |
| reportMissingModuleSource | missing-source |
| reportMissingParameterType | unannotated-parameter |
| reportMissingTypeStubs | untyped-import |
| reportNoOverloadImplementation | invalid-overload |
| reportOperatorIssue | unsupported-operation |
| reportPossiblyUnboundVariable | unbound-name |
| reportPrivateUsage | no-access |
| reportReturnType | bad-return |
| reportUnboundVariable | unbound-name |
| reportUndefinedVariable | unknown-name |
| reportUninitializedInstanceVariable | implicitly-defined-attribute |
| reportUnknownArgumentType | implicit-any |
| reportUnknownMemberType | implicit-any |
| reportUnknownParameterType | unannotated-parameter |
| reportUnknownVariableType | implicit-any |
| reportUnnecessaryCast | redundant-cast |
| reportUnusedCoroutine | unused-coroutine |
---
---
title: Migration Guides
description: How to switch from another type checker to Pyrefly
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
Welcome to the Pyrefly migration guide. This section provides resources to help you transition from other type checkers to Pyrefly.
## Migration Guides
- [Migrating from Mypy](migrating-from-mypy.mdx) - Guide for transitioning from Mypy to Pyrefly
- [Migrating from Pyright](migrating-from-pyright.mdx) - Guide for transitioning from Pyright to Pyrefly
Choose the appropriate guide based on your current type checker to get started with your migration to Pyrefly.
---
---
title: Pydantic Lax Mode Type Conversions
description: Complete reference of how Pyrefly converts types in Pydantic lax mode.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Pydantic Lax Mode Type Conversions
This page provides a complete reference for how Pyrefly converts types when working with Pydantic models in **lax mode** (the default). For background on how lax mode works, see the [main Pydantic documentation](../pydantic).
**Note:** Types without a specific conversion rule (e.g., `Callable`, `Any`, custom classes, and generic classes not listed below) are converted to `Any`.
---
## Atomic Type Conversions
Named unions are used for atomic types to keep type signatures concise.
| Input Type | Named Union | Expanded Type |
|------------|-------------|---------------|
| `int` | `LaxInt` | `int \| bool \| float \| str \| bytes \| Decimal` |
| `float` | `LaxFloat` | `int \| bool \| float \| str \| bytes \| Decimal` |
| `bool` | `LaxBool` | `bool \| int \| float \| str \| Decimal` |
| `Decimal` | `LaxDecimal` | `Decimal \| int \| float \| str` |
| `str` | `LaxStr` | `str \| bytes \| bytearray` |
| `bytes` | `LaxBytes` | `str \| bytes \| bytearray` |
| `date` | `LaxDate` | `date \| datetime \| int \| float \| str \| bytes \| Decimal` |
| `datetime` | `LaxDatetime` | `date \| datetime \| int \| float \| str \| bytes \| Decimal` |
| `time` | `LaxTime` | `time \| int \| float \| str \| bytes \| Decimal` |
| `timedelta` | `LaxTimedelta` | `timedelta \| int \| float \| str \| bytes \| Decimal` |
| `Path` | `LaxPath` | `Path \| str` |
| `UUID` | `LaxUuid` | `UUID \| str` |
| `None` | (no conversion) | `None` |
---
## Compositional Type Conversions
**Notation:**
- `T_converted` means the type `T` is recursively converted using lax mode rules (e.g., `int` → `LaxInt`)
- `T_flattened` means the type `T` is converted and expanded (e.g., `int` → `int | bool | float | str | bytes | Decimal`)
| Input Type/Container | Output Type/Container |
|----------------------|-----------------------|
| `type[T]` | `type[T_converted]` |
| `T1 \| T2 \| ...` | `T1_flattened \| T2_flattened \| ...` |
| `list[T]`, `set[T]`, `frozenset[T]`,
`Sequence[T]`, `Iterable[T]`, `deque[T]`,
`tuple[T, ...]` | `Iterable[T_converted]` |
| `tuple[T1, T2, ...]` | `Iterable[T1_flattened \| T2_flattened \| ...]` |
| `dict[K, V]` | `Mapping[K, V_converted]` |
**Examples:**
- **Type wrapper:** `type[int]` → `type[LaxInt]`
- **Union types:** Each member is converted and flattened. `int | bool` → `int | bool | float | str | bytes | Decimal`
- **Single-element containers and unbounded tuples:** Named unions are preserved. `list[int]` → `Iterable[LaxInt]`
- **Concrete tuples:** Element types are expanded and flattened. `tuple[int, str]` → `Iterable[int | bool | float | str | bytes | bytearray | Decimal]`
- **Dictionaries:** Only values are converted; keys remain unchanged. `dict[str, int]` → `Mapping[str, LaxInt]`
---
---
title: Pydantic Support
description: Pyrefly support for Pydantic.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Pydantic Support
Pyrefly includes **built-in support** for [Pydantic](https://pydantic.dev/), a popular Python library for data validation and parsing. This feature provides static type checking and IDE integration for Pydantic models.
> **Note:** Pyrefly's Pydantic support continues to evolve based on feedback and development. Please note that we support Pydantic v2 and above, which means that deprecated Pydantic v1 features are not included.
### Feedback
We welcome your feedback and suggestions. Please share your thoughts and ideas [here](https://github.com/facebook/pyrefly/issues/1078).
---
## 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.
- Recognizes `pydantic_settings.BaseSettings` for environment-driven configuration.
- Supports `@pydantic.dataclasses.dataclass`.
- 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](https://docs.pydantic.dev/latest/concepts/conversion_table/)):
```python
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]` becomes `Iterable[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](../pydantic-lax-conversions) page.
---
## Comparison to Existing Tools
[Mypy’s Pydantic plugin](https://docs.pydantic.dev/1.10/mypy_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:
1. Install `pydantic` (preferably v2).
1. Install `pyrefly` (version 0.33.0 or later).
1. Write your Pydantic models as usual.
1. 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](https://github.com/migeed-z/pyrefly-pydantic-demo) to try it out on some small examples.
---
## Some 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
```python
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
```python
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.
```python
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.
```python
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
```python
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
```python
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)
```
---
## Features Not Yet Supported
These are some Pydantic features that are **not yet supported**:
### Alias Generators
Pyrefly does not yet recognize `alias_generator` (or `AliasGenerator`) configured via `ConfigDict`. Fields will be type-checked using their original names rather than the generated aliases.
```python
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class Voice(BaseModel):
model_config = ConfigDict(alias_generator=to_camel)
first_name: str
# Not yet supported: pyrefly reports a missing-argument error for `first_name`
Voice(firstName="Alice")
```
---
---
id: pyrefly-faq
title: FAQ
slug: /pyrefly-faq
description:
Frequently Asked Questions about Pyrefly, a PEP 484 compliant Type Checker for
Python and IDE extension.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Pyrefly Frequently Asked Questions
## Why should I use Pyrefly over another type checker or language server?
Here are the main reasons to consider Pyrefly:
* **Rich IDE experience.** Pyrefly's language server includes advanced refactoring capabilities that were previously only available in Pylance or Pycharm. See [IDE Features](./IDE-features.mdx) for the full list of supported features.
* **Broad type system coverage & strong inference capabilities.** Pyrefly's type system implementation is mature, and the ability to infer types on un-annotated code allows Pyrefly to provide type safety while reducing the need for manual annotations.
* **Battle-tested at scale.** Pyrefly runs in CI for Instagram and PyTorch, so it's proven on large, real-world codebases. It's also the default Python language server for the Antigravity and Positron editors.
* **Built-in Pydantic and Django support.** Pyrefly has native support for [Pydantic](./pydantic.mdx) and [Django](./django.mdx), handling special type-checking behavior that can't be expressed in the current type system. Mypy is the only other type checker that supports these packages, via third-party plugins.
* **Useful peripheral tools:** Pyrefly comes with a suite of useful tools to help you get the most out of your type annotations:
* [`pyrefly coverage`](./report.mdx) to measure type coverage across your codebase and enforce it in CI.
* [Error baselines](./error-suppressions.mdx#baseline-files-experimental) to track and suppress pre-existing errors without modifying source code.
* [`pyrefly infer`](./autotype.mdx) to automatically add inferred type annotations to your code.
* [`pyrefly init`](./migrating-to-pyrefly.mdx) for automatic config migration from mypy or pyright.
* [`pyrefly suppress`](./error-suppressions.mdx#upgrading-pyrefly-and-other-changes-that-introduce-new-type-errors) for automatic bulk error suppression during upgrades.
## How do I pronounce Pyrefly?
It's pronounced PIE-ur-fly, rhyming with "firefly."
## What is the relationship to Pyre?
Pyrefly is a ground-up rebuild that doesn’t share any core type checking code with Pyre. Not only is Pyrefly written in a new language (Rust instead of OCaml), but its design deviates in a major way from [Pyre](https://pyre-check.org/). Rust enables us to deliver substantial performance improvements and support multiple operating systems (including Windows). Beyond the core type checker itself, there are helper tooling and many lessons learned that we will take from Pyre and the community of Python type checking maintainers who have done tremendous work to get the state of type checking to where it is today.
## Is Pyrefly a type checker or a language server?
Yes 😉
Pyrefly is both of these things, and you can use one without the other if you choose.
* type checking: Pyrefly can be used as a standalone type checker - directly run in your terminal, added to your CI or integrated into your IDE via an extension (made possible by leveraging language server capabilities)
* language server: Pyrefly can be used as a standalone Python language server, integrated into your IDE with all the typical IDE features you would expect (hover, go-to-definition etc.). You can use it with or without type checking enabled.
## Yet another Type Checker! Why not improve the ones adopted by the community already?
We are standing on the shoulders of giants. The contributions to Python typing by Mypy, Pyright, Pytype, Pyre and others have been invaluable. We borrowed concepts and learned from them as we rolled our own. Open source conformance matters to us a lot. While we might make some opinionated decisions, we'll adhere to the PEP process. Pyre was the only type checker that could scale for Meta’s needs and was starting to show its age, so we started with a ground-up rewrite aimed at usability and performance.
We built a custom engine for incremental computation and designed our type-checking algorithm based on years of experience in gradual typing theory and Rust expertise. By open-sourcing this technology we hope it can serve projects of any size well.
## Why Rust?
We would have preferred to write Pyrefly in Python, but we didn't think we could hit our ambitious performance goals using Python today (hopefully future work around free-threaded Python and JIT changes that). After ruling out Python, we wanted something that was safe, cross-platform, compiled to WASM (for a Playground experience). Rust and Go are probably the best choices for those goals, and our team at Meta had more experience with Rust.
## Where do I report bugs?
Please open an [issue on our GitHub](https://github.com/facebook/pyrefly/issues) page. You can leave feature requests there as well :) Our current goal is to get through our first major milestone, and after that we can look at bugs and features beyond the current roadmap.
## Can I contribute to Pyrefly?
Please see the: [contributing guidelines](https://github.com/facebook/pyrefly/blob/main/CONTRIBUTING.md).
## How do I know this project won't go unmaintained after a year?
Great question. We have made a substantial investment in Pyrefly, use it internally and aligned open source maintainability as a key principle for starting this project in the first place. We’re in it for the long haul. Using it is the best way to encourage further investment from our leadership.
## This is cool, I want to learn more about the technical details.
See our [README.md](https://github.com/facebook/pyrefly/blob/main/README.md) for the high level design. We plan to add more detailed documentation along with announcements on [https://engineering.fb.com](https://engineering.fb.com/)
## I don't like Python's Type System. Stop wasting your time.
Tell us more - seriously! We want to hear your objections to typing. We hope that better tooling, improvements to the type system and well typed libraries will help provide make development easier. If all else fails our fast code navigation and inference algorithm might spark joy in your IDE, so give us a chance.
_Curious about something else or just want to chat about bolting types onto Python, join us on [Discord](https://discord.gg/Cf7mFQtW7W)._
---
---
title: Typing Features and PEPS
description: Typing features and associated PEPs available in each Python version.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
*Typing Features and PEPS available in each Python Version.*
| Feature (click PEP for details) | What it adds / looks like | Introduced in |
| --- | --- | --- |
| [PEP 484](https://peps.python.org/pep-0484/) -- Core **type hints** & `typing` module | `def add(a: int, b: int) -> int:` | **3.5** |
| [PEP 526](https://peps.python.org/pep-0526/) -- **Variable annotations** | `count: int = 0` | **3.6** |
| [PEP 563](https://peps.python.org/pep-0563/) -- `from __future__ import annotations` (lazy eval) | Annotations stored as **strings** | **3.7** (future‑flag) |
| [PEP 544](https://peps.python.org/pep-0544/) -- **Protocols** (structural typing) | `class Jsonable(Protocol): ...` | **3.8** |
| [PEP 589](https://peps.python.org/pep-0589/) -- **TypedDict** | `class User(TypedDict): ...` | **3.8** |
| [PEP 586](https://peps.python.org/pep-0586/) -- **Literal** types | `def log(level: Literal["info","warn"]): ...` | **3.8** |
| [PEP 591](https://peps.python.org/pep-0591/) -- **Final** qualifier | `TOKEN: Final[str] = "..."` | **3.8** |
| [PEP 585](https://peps.python.org/pep-0585/) -- **Built‑in generics** | `list[int]`, `dict[str, Any]` | **3.9** |
| [PEP 593](https://peps.python.org/pep-0593/) -- **Annotated** | `x: Annotated[int, "units=px"]` | **3.9** |
| [PEP 604](https://peps.python.org/pep-0604/) -- **Union** syntax | `int \| None` | **3.10** |
| [PEP 612](https://peps.python.org/pep-0612/) -- **ParamSpec / Concatenate** | decorator‑safe generics | **3.10** |
| [PEP 613](https://peps.python.org/pep-0613/) -- `TypeAlias` qualifier | `Vector: TypeAlias = list[float]` | **3.10** |
| [PEP 647](https://peps.python.org/pep-0647/) -- **TypeGuard** for narrowing | `def is_str(x) -> TypeGuard[str]: ...` | **3.10** |
| [PEP 655](https://peps.python.org/pep-0655/) -- `Required` / `NotRequired` for **TypedDict** | optional vs. mandatory keys | **3.11** |
| [PEP 646](https://peps.python.org/pep-0646/) -- **Variadic generics** (`TypeVarTuple`, `Unpack`) | tensor shapes, 2‑D arrays, ... | **3.11** |
| [PEP 673](https://peps.python.org/pep-0673/) -- **Self** type | fluent APIs: `def set(...) -> Self:` | **3.11** |
| [PEP 681](https://peps.python.org/pep-0681/) -- **dataclass_transform** helper | libraries like Pydantic, attrs | **3.11** |
| [PEP 695](https://peps.python.org/pep-0695/) -- **Class‑level generics syntax** | `class Box[T]: ...` | **3.12** |
| [PEP 698](https://peps.python.org/pep-0698/) -- `@override` decorator | flag intentional overrides | **3.13** |
| [PEP 649](https://peps.python.org/pep-0649/) -- *New* deferred‑eval algorithm (replaces PEP 563) | becomes the default | **3.14** |
---
---
title: Python Typing 101
description: A gentle, example‑driven introduction to static type hints in Python.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
import CodeSnippet from '@site/src/sandbox/CodeSnippet'
# Python Typing 101
*A beginner‑friendly guide to adding type hints in Python.*
**Note:** This tutorial assumes you understand some basic Python syntax, but are new to programming with type hints. To learn more about Python, see the [Python Tutorial](https://docs.python.org/3/tutorial/) and [Getting Started Guide](https://www.python.org/about/gettingstarted/)
## 1. What is a Type?
A type is a classification that defines what operations can be performed on a piece of data, what values it can hold, and how it behaves in memory. Types are fundamental to programming because they help ensure that operations on data make sense.
For example:
- An `int` (integer) type can be added, subtracted, or multiplied
- A `str` (string) type can be concatenated or split
- A `list` type can be indexed, sliced, or iterated over
**Note:** These are just examples of common operations for each data type. Python's built-in types support many more operations that are not listed here.
Understanding types helps you predict how your code will behave and avoid runtime errors from trying to perform operations that don't make sense, such as dividing a string by a number.
## 2. What is a Type Hint in Python?
A type hint in Python is a way to indicate the expected data type of a variable, function parameter, or return value. It's a hint to other developers (and to tools like type checkers and IDEs) about what type of data should be used with a particular piece of code.
Type hints are **not enforced at runtime by Python itself,** but they can be used by third-party tools (like Pyrefly) to catch type-related errors before your code runs. They also serve as documentation, making it easier for others to understand how to use your code.
Here's an example of a simple function with type hints:
```
def greet(name: str) -> None:
print(f"Hello, {name}!")
```
In this example:
- `name: str` indicates that the `name` parameter should be a string.
- `-> None` specifies that the function doesn't return any value (similar to `void` in other languages).
## 3. Why Bother with Type Hints?
Python is a dynamically typed language, which means you can write code without declaring types. However, this can lead to bugs or ambiguity in your code.
_TL;DR_
* Catch bugs **before** running the code.
* Improve editor autocomplete & refactors.
* Turn your code into living documentation.
str:
return text * times
`}
/>
In this example:
- The first function lacks type hints, making it unclear what types `text` and `times` should be. The `*` operator works differently depending on types (string repetition, list repetition, or multiplication).
- The second function uses type hints to clearly indicate that `text` should be a string, `times` should be an integer, and the function returns a string.
- This clarity helps prevent bugs like accidentally passing a string for `times` or using the function incorrectly.
### Can you spot the bug?
```
class Rectangle:
width: int
height: int
def __init__(self, width: int, height: int) -> None:
self.width = width
self.height = height
rect = Rectangle(width=100, height=50)
area = rect.width * rect.hieght
print(area)
```
In this example:
- The bug is a typo in `rect.hieght` (should be `rect.height`).
- Without type hints, Python would only report this error at runtime when it tries to access the non-existent attribute.
- With type hints and a tool like Pyrefly, this error would be caught before running the code because the `Rectangle` class has defined attributes `width` and `height`, but not `hieght`.
**Spelling is hard!** Let's add the `dataclass` decorator to our class definition. This will generate a constructor for us, and also add a few other useful methods.
In this dataclass example:
- The `@dataclass` decorator automatically generates methods like `__init__`, `__repr__`, and `__eq__` based on the class attributes.
- Type hints are used to define the class attributes (`width: int`, `height: int`).
- The same spelling error exists (`rect.hieght`), but tools like Pyrefly can catch this before runtime because the dataclass clearly defines which attributes exist.
- This demonstrates how type hints combined with dataclasses provide both convenience and better error detection.
## 4. Primitive Types
Since Python 3.9 you can use all the [primitive types](https://docs.python.org/3/library/stdtypes.html) directly as annotations.
In this primitive types example:
- Each variable is annotated with its expected type (`int`, `float`, `str`, `bool`).
- The values assigned match their declared types.
- These annotations help document the code and allow type checkers to verify that operations on these variables are valid for their types.
- For example, a type checker would flag an error if you tried `age + name` since adding an integer and string isn't a valid operation.
You can also specify a parameter as optional by using `Optional` type, or now with the `| None` syntax.
In this Optional type example:
- Both variables can either be a string or `None`.
- `Optional[str]` is the traditional syntax (pre-Python 3.10).
- `str | None` is the newer union syntax introduced in Python 3.10.
- These annotations tell type checkers that the variable might be `None`, so they can warn you if you try to perform string operations without checking for `None` first.
## 5. Collections
### Syntax Examples
- List of numbers `list[int] scores: list[int] = [98, 87, 91]`
- Tuple of two floats `tuple[float, float] point: tuple[float, float] = (3.0, 4.0)`
- Dict of str -> int `dict[str, int] inventory: dict[str, int] = {"apples": 5}`
- Set of strings `set[str] authors: set[str] = {"Bob", "Eve"}`
Since Python 3.9 you can subscript built‑ins directly—no need for `from typing import List`.
## 6. Functions
int:
return a + b
`}
/>
In this basic function example:
- Both parameters `a` and `b` are annotated as integers.
- The function is annotated to return an integer (`-> int`).
- This tells type checkers that the function should only be called with integers and that the return value should only be used in contexts where an integer is expected.
Default values keep their annotation:
str:
return "Hello!" if polite else f"Yo {name}"
`}
/>
In this function with default values:
- The `name` parameter must be a string.
- The `polite` parameter is a boolean with a default value of `True`.
- The function returns a string.
- Even though `polite` has a default value, it still has a type annotation to ensure that if it's explicitly provided, it must be a boolean.
Variable‑length arguments:
None:
for m in msgs:
if log is not None:
log(m)
else:
print(m)
`}
/>
In this variable-length arguments example:
- `Logger` is defined as a type alias for a callable that takes a string and returns nothing (`None`).
- `*msgs: str` indicates that the function accepts any number of string arguments.
- `log: Logger | None` means the `log` parameter can be either a Logger function or `None`.
- The function is annotated to return `None`.
- This demonstrates how to type complex function signatures with variable arguments and function parameters.
## 7. Get Type Hint Signals Directly in Your Editor
You can download the [Pyrefly extension for VSCode](https://marketplace.visualstudio.com/items?itemName=meta.pyrefly) to get type hint signals directly in your IDE.
Next, install [Pyrefly](../installation/) and check some code:
```
# Fast, zero‑config
pip install pyrefly
pyrefly check ./my_sample.py
# Check whole directories
pyrefly check app/ tests/
```
Create a `pyrefly.toml` file to configure your project. Instructions [here](../configuration).
---
---
title: Coverage
description: Measure and enforce type coverage in your Python codebase with Pyrefly.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Pyrefly Coverage
`pyrefly coverage` measures how much of your code is annotated with types. It has two subcommands:
- [`pyrefly coverage check`](#checking-coverage-against-a-threshold) fails when coverage is below
a threshold, useful as a CI gate.
- [`pyrefly coverage report`](#generating-a-json-report) emits a JSON report with per-module
statistics.
:::warning Experimental
The output format and behavior may change in future releases without notice.
:::
## How coverage is measured
Coverage is counted over _typables_: function return types, function parameters, module-level
variables, and class attributes. Each typable is either **typed** (annotated, no `Any` in the
resolved type), **any** (annotated, but the resolved type contains `Any`), or **untyped** (no
annotation).
- **Coverage** is the percentage of typables that are annotated; `Any` counts as covered.
- **Strict coverage** is the percentage of typables that are typed; `Any` doesn't count.
Only public symbols count: names without a leading underscore, plus anything in `__all__`.
`self`/`cls` are not typable, overloads are merged, and `.py` files are skipped when their `.pyi`
stub is also analyzed (`--prefer-stubs=false` disables this). With `--public-only`, only symbols
reachable from public modules via re-export chains count: the library's public API.
## Checking coverage against a threshold
```sh
pyrefly coverage check robot/ --fail-under 60
```
On success it prints a one-line summary and exits 0:
```
INFO type coverage 66.67% (4 of 6 typable)
```
Otherwise it exits non-zero and reports every offending symbol in the same style as
`pyrefly check`, as `coverage-missing` (no annotations at all) or `coverage-partial` (only some):
```
WARN `walk` is not fully typed [coverage-partial]
--> robot/legs.py:1:1
|
1 | / def walk(speed, direction: str) -> None:
2 | | print(f"walking {speed} {direction}")
| |_________________________________________-
|
WARN `stop` is untyped [coverage-missing]
--> robot/legs.py:5:1
|
5 | / def stop():
6 | | print("stopping")
| |_____________________-
|
ERROR type coverage 66.67% (4 of 6 typable) is below the 100.00% threshold
```
With `--strict`, annotations that resolve to `Any` count as untyped. The findings format is
configurable with `--output-format`. See `pyrefly coverage check --help` for all flags.
## Generating a JSON report
```sh
pyrefly coverage report path/to/directory/ > coverage.json
```
The report contains a `"major.minor"` `schema_version`, a `summary`, and one entry per module in
`module_reports`, with:
- per-symbol typed/any/untyped counts and locations (`symbol_reports`)
- the module's public names (`names`)
- error-suppression comments (`type_ignores`)
- typable totals, `coverage` and `strict_coverage` percentages, and counts of functions, methods,
parameters, classes, attributes, and properties
See `pyrefly coverage report --help` for all flags. The output is easy to consume with e.g. `jq`:
```sh
pyrefly coverage report path/to/directory/ | jq .summary.strict_coverage
```
---
---
title: Stub Generation
description: Generate .pyi stub files from Python source files with Pyrefly stubgen.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Pyrefly Stubgen
:::warning Experimental
This feature is experimental and under active development. The output format and behavior may change in future releases.
:::
Pyrefly can generate `.pyi` [stub files](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html) from Python source files using static analysis. Stub files provide type information for modules without requiring the source code to be fully annotated.
Unlike mypy's stubgen, Pyrefly operates entirely through static analysis — it does not inspect code at runtime.
## Usage
```
pyrefly stubgen path/to/file.py
# or
pyrefly stubgen path/to/directory/
```
By default, generated stubs are written to the `out/` directory, mirroring the source directory structure.
## Flags
| Flag | Default | Description |
|------|---------|-------------|
| `-o, --output-dir` | `out` | Output directory for generated `.pyi` files |
| `--include-private` | off | Include names with a single leading underscore |
| `--include-docstrings` | off | Preserve docstrings in generated stubs |
## What gets included
Stubgen extracts the public API of each module:
- **Functions and methods** — signatures with parameter types, defaults, and return types
- **Classes** — base classes, decorators, attributes, and methods
- **Variables** — module-level and class-level variables with type annotations
- **Imports** — preserved from source
- **Type aliases** — preserved from source
Private names (single leading underscore) are excluded by default. Dunder names (`__init__`, `__repr__`, etc.) are always included.
## Type resolution
Pyrefly uses its type checker to resolve types for declarations that lack explicit annotations. When a type cannot be resolved, it is annotated as `Incomplete` from `_typeshed`, following the convention used by typeshed stubs.
Source annotations are always preferred over inferred types when both are available.
---
---
title: Agent Skill for Tensor Shape Porting
description: How to use the port-model agent skill to automatically add tensor shape annotations to PyTorch models.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Agent Skill for Tensor Shape Porting
Pyrefly includes a skill for [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview)
that can automatically port PyTorch models to use tensor shape annotations. The
skill runs a structured, multi-step workflow — auditing ops, inventorying
parameters, probing shapes with `reveal_type`, restructuring for tracking, and
verifying the result — that produces a fully annotated port file.
## Invoking the skill
In Claude Code, from a project with pyrefly's tensor shape fixtures, ask:
```
Port this model to use tensor shape types: path/to/model.py
```
Or use the slash command directly:
```
/port-model path/to/model.py
```
The skill will:
1. **Audit ops** — check every `nn.Module` and `torch`/`F.` function against
fixture stubs and the DSL registry, adding missing stubs before proceeding.
2. **Inventory the original** — list every class, method, and function, noting
which constructor parameters are `Dim` vs `int`.
3. **Port each module in dependency order** — for each module, inventory
parameters, type the constructor, probe the forward with `reveal_type`,
restructure bare results for tracking, write the forward with `assert_type`,
and fill a post-module checklist.
4. **Verify** — run `verify_port.sh`, audit bare `assert_type` calls, compare
against the style guide, and produce a completion report.
## The iterate-and-reflect loop
A single pass usually gets 80-90% of the way there. The remaining gaps —
bridge dims, optional dims, parameterized configs — benefit from a second pass
where the AI reflects on what it missed.
The workflow:
1. **First pass.** Invoke the skill. Review the completion report, paying
attention to the bare `assert_type` fraction and any `type: ignore` counts.
2. **Reflect.** Ask the AI to re-read the skill file and compare what it did
against what the skill prescribes. A prompt like:
```
Re-read the port-model skill and reflect on the port you just produced.
What did you miss? What could be improved?
```
This typically surfaces missed restructuring opportunities — cases where the
AI used a bare `assert_type` or annotation fallback without attempting all
the restructuring steps in Step 4.
3. **Second pass.** Ask it to fix the issues it identified. This usually
converges: the bare fraction drops and shaped coverage improves.
Most models converge in 2-3 iterations. Complex models with many dynamic
patterns (e.g., `nn.Sequential(*list)`, conditional branching, late-initialized
buffers) may need one more round.
## Reading the output
The skill produces a `verify_port.sh` report with these key metrics:
| Metric | What it means |
|--------|--------------|
| **`ig` (ignore)** | `type: ignore` comments. Lower is better. Each should have a category (algebraic gap, conditional equality, stub gap). |
| **`bs` (bare sig)** | Bare `Tensor` in function signatures. Should be 0 for well-typed modules. |
| **`bv` (bare var)** | Bare `Tensor` in local variable annotations. Lower is better. |
| **`sh` (shaped)** | `assert_type` calls with full shapes (e.g., `Tensor[B, D]`). Higher is better. |
| **`ba` (bare assert)** | `assert_type(x, Tensor)` — tracking gaps. Lower is better. Each should have a root-cause comment. |
| **`sm` (smoke)** | Smoke test functions. At least 1-2 per model. |
A good port has high `sh`, low `ba`, and every bare assert documented with a
root cause tracing back to a restructuring receipt.
## When to intervene
The AI workflow handles most patterns automatically, but some decisions require
domain knowledge:
- **Bridge dims.** When an untracked section (e.g., a feature extractor built
with `nn.Sequential(*list)`) connects to a tracked downstream module, you may
need to decide which dimension to promote to a class type parameter.
- **Missing stubs.** If the model uses a library without fixture stubs, you'll
need to add minimal stubs for the specific ops used.
- **Architectural choices.** When a model has multiple valid ways to restructure
for tracking (e.g., separating the first iteration of a loop vs. using a typed
interface), domain knowledge about the model's invariants can guide the better
choice.
- **Config parameterization.** Deciding which config fields should become `Dim`
type parameters (and which modules extract which parameters) is often a design
decision about the model's API.
---
---
title: Contributing
description: How to extend tensor shape coverage by adding fixture stubs, DSL specifications, and ported models.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Contributing to Tensor Shapes
Pyrefly's tensor shape tracking is designed so that coverage of the PyTorch
library can be extended without understanding Pyrefly's internals. There are
three ways to contribute:
1. **Fixture stubs** — `.pyi` files with shape-generic type signatures for
PyTorch modules and functions (e.g., `nn.Linear`, `torch.mm`).
2. **DSL functions** — shape transform specifications for operators with
complex shape logic (e.g., `reshape`, `cat`, `F.interpolate`).
3. **Ported models** — fully annotated ports of real-world PyTorch models
that serve as tests and examples.
For detailed instructions on writing stubs, DSL functions, and porting
models, see the
[Tensor Shapes Contributing Guide](https://github.com/facebook/pyrefly/blob/main/TENSOR_SHAPES_CONTRIBUTING.md)
in the repository.
---
---
title: API Reference
description: Complete reference for Dim, Tensor, and tensor shape type system APIs.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# API Reference
This page documents the types, functions, and type-level constructs that make
up pyrefly's tensor shape type system.
```sandbox
dir: tensor-shapes-reference
description: Experiment with Dim operators, type-level arithmetic in shapes, and concatenation.
```
## `Dim[X]`
`Dim[X]` is a type constructor that bridges runtime integer values to
type-level symbols. It is defined in the `shape_extensions` package.
### Basics
`Dim[X]` denotes the type of an integer value whose type-level identity is
`X`. For example:
- `Dim[5]` is the type of the literal `5`
- `Dim[N]` (where `N` is a type variable) is the type of an integer whose
value is bound to `N` at the type level
`Dim` is a subtype of `int`, so `Dim` values can be used anywhere `int` is
expected. However, the reverse is not true — passing a plain `int` where
`Dim[X]` is expected loses tracking.
### Arithmetic
Arithmetic on `Dim` values produces `Dim` results with the corresponding
type-level expression:
| Expression | Type |
|-----------|------|
| `a + b` where `a: Dim[A]`, `b: Dim[B]` | `Dim[A + B]` |
| `a - b` | `Dim[A - B]` |
| `a * b` | `Dim[A * B]` |
| `a // b` | `Dim[A // B]` |
| `a ** b` | `Dim[A ** B]` |
**Caution:** `int * Dim` produces `Unknown` because the `int` side has no
type-level identity. Use `Dim * Dim` or literal `* Dim` instead.
### `Dim[X] | None`
For optional dimensions — parameters that may or may not be present — use
`Dim[X] | None`. In the forward method, narrow with
`if param is not None:` to recover `Dim[X]` inside the branch:
```python
class Attention[D, RK](nn.Module):
def __init__(self, dim: Dim[D], rank_k: Dim[RK] | None = None):
...
def forward[B, T](self, x: Tensor[B, T, D]) -> Tensor[B, T, D]:
if self.rank_k is not None:
# rank_k is Dim[RK] here
...
```
### Usage patterns
| Pattern | Purpose |
|---------|---------|
| `def __init__(self, dim: Dim[D])` | Accept a dimension as a constructor parameter |
| `class Model[D](nn.Module)` | Make a dimension a class-level type parameter |
| `def forward[B](self, x: Tensor[B, D])` | Bind a per-call dimension |
| `self.head_dim = dim // n_head` | Compute a derived dimension (`Dim[D // NHead]`) |
## `Tensor[D1, D2, ...]`
`Tensor` with type arguments represents a tensor with a known shape. The
type arguments are the dimensions, in order.
### Forms
| Form | Meaning |
|------|---------|
| `Tensor[3, 4]` | Concrete 2D tensor with shape `(3, 4)` |
| `Tensor[B, C, H, W]` | Generic 4D tensor with symbolic dimensions |
| `Tensor[B, 3 * C, H // 2]` | Dimensions can contain arithmetic expressions |
| `Tensor[*Bs, D]` | Variadic: any number of leading batch dimensions |
| `Tensor` (bare) | Shape unknown — tracking gap |
### Variadic dimensions with `*Bs`
Use `TypeVarTuple` (or PEP 646 `*Bs` syntax) for dimensions that should be
propagated without being enumerated:
```python
def forward[*Bs](self, x: Tensor[*Bs, InDim]) -> Tensor[*Bs, OutDim]:
...
```
This accepts any number of leading dimensions (batch, sequence, etc.) and
preserves them in the output.
**Don't hide known class dims inside variadic params.** If the module has a
class-level Dim `D`, use `Tensor[*Bs, D]` not `Tensor[*S]`.
### `.shape` and `.size()`
When `x: Tensor[B, C, H, W]`:
- `x.shape` has type `tuple[Dim[B], Dim[C], Dim[H], Dim[W]]`
- `x.size(0)` has type `Dim[B]`
- `x.size()` has type `tuple[Dim[B], Dim[C], Dim[H], Dim[W]]`
This means you can extract dimensions from tensors and use them to construct
new tensors with matching shapes.
## `assert_type`
`assert_type(expr, Type)` is checked by the type checker: it verifies that
`expr` has exactly the stated type. If the types don't match, the checker
reports an error.
```python
h = self.fc1(x)
assert_type(h, Tensor[B, 512]) # checked by pyrefly
```
Use `assert_type` during development to verify inferred shapes as you port
a model. Once the port is complete, remove the `assert_type` calls — each
one corresponds to an inlay type hint that your IDE shows permanently.
Pyrefly catches shape errors through function signatures and return types
regardless.
`assert_type` forces evaluation of its type argument at runtime, so a file
with `assert_type` calls will crash if executed. This is fine during
development (you run `pyrefly check`, not the file itself) — just remove
them when the port is done.
### When to use
- During porting, after key shape-changing operations (reshapes,
convolutions, matmuls)
- As regression guards for complex shape computations
In practice, pyrefly shows inferred shapes as inlay type hints in your
editor, so you can verify shapes visually. Use `assert_type` at key
checkpoints where you want a permanent regression guard.
### `reveal_type`
`reveal_type(expr)` prints the inferred type of `expr` during type checking.
Use it to understand what pyrefly infers before writing `assert_type`:
```python
h = self.fc1(x)
reveal_type(h) # Revealed type: Tensor[B, 512]
```
Replace `reveal_type` with `assert_type` once you know the expected type.
## Type-level arithmetic
Annotations can contain arithmetic on type parameters and literals:
| Expression | Example |
|-----------|---------|
| Addition | `Tensor[B, C1 + C2, H, W]` — concatenation |
| Subtraction | `Tensor[B, T, D - 1]` |
| Multiplication | `Tensor[B, NHead * DK]` — multi-head reshape |
| Floor division | `Tensor[B, NHead, T, D // NHead]` |
| Exponentiation | `Tensor[B, C * 2 ** I, H // 2 ** I]` |
### Simplification rules
The type checker automatically simplifies expressions:
- `2 * C // 2` → `C`
- `(H - 1) * 2 + 2` → `H * 2`
- `(a * b) // b` → `a` (sound for all positive integers)
### Known limitations
`N * (X // N)` does **not** simplify to `X` — floor division loses the
remainder, so the equivalence only holds when `X` is divisible by `N`.
The checker can't assume this. Common instances:
- Multi-head reassembly: `NHead * (D // NHead)` — use `type: ignore`
- BiLSTM output: `2 * (D // 2)` — use `type: ignore`
## Annotation hierarchy
When annotating local variables, choose from most to least desirable:
1. **`assert_type`** — verifies the checker's inference. Proves the system
works, not just that you annotated correctly.
2. **Annotation fallback** — `x: Tensor[B, C, H, W] = untracked_op(...)`.
The checker can't infer the shape, but the annotation is compatible.
Document WHY.
3. **`type: ignore`** — the checker produces a WRONG type (algebraic gap).
Last resort. Always include a comment explaining the specific gap.
4. **Bare `Tensor`** — shape genuinely unknowable (data-dependent token
counts, conditional accumulation). Document the specific reason.
## Jaxtyping compatibility
Pyrefly supports [jaxtyping](https://github.com/patrick-kidger/jaxtyping)
annotations as an alternative front-end:
| Pyrefly native | Jaxtyping equivalent |
|---------------|---------------------|
| `Tensor[M, 2, M // 2]` | `Shaped[Tensor, "M 2 M//2"]` |
| `Tensor[B, C, H, W]` | `Shaped[Tensor, "B C H W"]` |
Jaxtyping annotations are translated internally to generics and display
back in jaxtyping syntax. Note that jaxtyping cannot share symbolic
dimensions across class boundaries — see the
[overview](./tensor-shapes.mdx#jaxtyping-runtime-type-checking) for details.
---
---
title: Getting Started
description: How to configure Pyrefly for tensor shape checking and set up your project.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Getting Started with Pyrefly Tensor Shapes
This page walks you through configuring Pyrefly for tensor shape checking and
getting your first shape-annotated code running.
## Configuration
Tensor shape checking requires shape-aware stubs to be available through normal
import resolution. For a local setup, add the fixture stub directory to your
`pyrefly.toml` (or under `[tool.pyrefly]` in `pyproject.toml`):
```toml
search_path = [
"path/to/fixtures",
]
```
Tensor shape support is enabled automatically when Pyrefly can resolve the
`shape_extensions` package. The fixture directory provides `shape_extensions`
and shape-aware versions of library stubs.
**`search_path`** points to a directory of *fixture stubs* — `.pyi` files that
provide shape-generic type signatures for PyTorch modules and functions. The
real `torch` library's type stubs don't carry shape information, so the fixtures
replace them with shape-aware versions (e.g., `nn.Conv2d.__init__` that captures
kernel size, stride, and padding as type-level values, and a `forward` that
computes the output spatial dimensions).
The fixtures live in Pyrefly's source tree at
[`test/tensor_shapes/fixtures/`](https://github.com/facebook/pyrefly/tree/main/test/tensor_shapes/fixtures).
To use them in your project, copy the `fixtures/` directory into your project
and set `search_path` to point to it. The path is relative to the location of
your `pyrefly.toml`.
The fixtures also provide the `shape_extensions` package, which exports `Dim` — the
bridge between runtime integer values and type-level symbols.
## Imports and runtime considerations
Python evaluates type annotations at runtime by default. This is a problem
for tensor shape annotations because Python's built-in `typing.TypeVar`
doesn't support arithmetic — expressions like `D // NHead` in an annotation
will raise `TypeError` when the annotation is evaluated. There are two ways
to avoid this:
### Option 1: `from __future__ import annotations` (recommended)
Adding this import at the top of the file defers evaluation of all
annotations, so shape arithmetic never executes at runtime:
```python
from __future__ import annotations
import torch
import torch.nn as nn
from torch import Tensor
from shape_extensions import Dim
```
This works with both old-style and new-style generics (PEP 695
`class Foo[T]` syntax).
**`assert_type` during development:** You can use `assert_type` while
porting to verify shapes via `pyrefly check`. Once you're done, remove the
`assert_type` calls — each one corresponds to an IDE inlay type hint that
shows the same information permanently. Pyrefly catches shape errors
through your function signatures and return types regardless.
Note that `assert_type` forces evaluation of its type argument, so the
file will crash if you try to *run* it with `assert_type` calls still
present. This is fine — just remove them when the port is complete.
You can also guard `Tensor` and `Dim` under `TYPE_CHECKING` if you prefer
to keep shape imports invisible at runtime:
```python
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
import torch.nn as nn
if TYPE_CHECKING:
from torch import Tensor
from shape_extensions import Dim
```
### Option 2: `shape_extensions.TypeVar` (runtime-compatible)
If you need annotations to evaluate at runtime (e.g., for runtime shape
validation or keeping `assert_type` in production code), import
`shape_extensions` directly. The package patches
`torch.Tensor`, `nn.Conv2d`, and other torch classes to accept subscript
syntax at runtime without crashing. It also provides a `TypeVar` that
supports arithmetic (`N + 1` returns `self` instead of raising `TypeError`).
Use old-style generics with `shape_extensions.TypeVar`:
```python
from typing import assert_type
import torch
import torch.nn as nn
from torch import Tensor
from shape_extensions import Dim, TypeVar
N = TypeVar("N")
M = TypeVar("M")
class Linear(nn.Module):
def __init__(self, n: Dim[N], m: Dim[M]):
...
```
PEP 695 new-style generics (`class Foo[T]`) automatically use
`typing.TypeVar` internally, which doesn't support arithmetic — so this
option requires old-style generics.
```sandbox
dir: tensor-shapes-setup
description: Experiment with Dim arithmetic, TYPE_CHECKING imports, and building typed tensors — no setup required.
```
## Hello world
Here's a minimal example to verify everything works. This example uses
Option 1 (`from __future__ import annotations`) since it's the simplest
setup. We skip `assert_type` — instead, run `pyrefly check` and use your
IDE's inlay type hints to verify shapes.
Create a file `hello_shapes.py`:
```python
from __future__ import annotations
import torch
import torch.nn as nn
from torch import Tensor
from shape_extensions import Dim
class TwoLayerNet[InDim, HidDim, OutDim](nn.Module):
def __init__(
self,
in_dim: Dim[InDim],
hid_dim: Dim[HidDim],
out_dim: Dim[OutDim],
):
super().__init__()
self.fc1 = nn.Linear(in_dim, hid_dim)
self.fc2 = nn.Linear(hid_dim, out_dim)
def forward[B](self, x: Tensor[B, InDim]) -> Tensor[B, OutDim]:
h = self.fc1(x) # pyrefly infers: Tensor[B, HidDim]
return self.fc2(torch.relu(h))
```
Run `pyrefly check hello_shapes.py`. You should see no errors — pyrefly
infers the shapes through the `nn.Linear` calls.
If you're using an IDE with Pyrefly's language server, you'll see inlay
type hints showing the inferred shape of `h` as `Tensor[B, HidDim]`
without needing any `assert_type` calls.
### Inlay hints in action
Here's what inlay hints look like on a real model (NanoGPT). The MLP
module shows shapes flowing through linear layers and activations:
The forward method signature shows how `x.size()` unpacks into typed
dimensions:
And the attention module, where view/transpose reshapes for multi-head
attention are fully tracked:
The full attention body, including both flash and manual paths:
---
---
title: "Tutorial 4: Configs and Dynamic Patterns"
description: Parameterized config dataclasses, dynamic construction patterns, and typed interfaces.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Tutorial 4: Configs and Dynamic Patterns
The previous tutorials covered models where dimensions flow directly through
constructor parameters. In practice, many models store hyperparameters in
config dataclasses and construct modules dynamically. This tutorial shows how
to type those patterns.
## Config classes with type parameters
When a `@dataclass` holds dimension hyperparameters consumed by multiple
modules, make it generic so dimensions propagate through constructors:
```python
@dataclass
class GPTConfig[VocabSize, BlockSize, NEmbedding, NHead, NLayer]:
block_size: Dim[BlockSize]
vocab_size: Dim[VocabSize]
n_layer: Dim[NLayer]
n_head: Dim[NHead]
n_embd: Dim[NEmbedding]
dropout: float = 0.0
bias: bool = True
```
Modules extract only the type parameters they need, using `Any` for the rest:
```python
class MLP[NEmbedding](nn.Module):
def __init__(self, config: GPTConfig[Any, Any, NEmbedding, Any, Any]):
super().__init__()
self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd)
self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd)
def forward[B, T](
self, x: Tensor[B, T, NEmbedding]
) -> Tensor[B, T, NEmbedding]:
h = F.gelu(self.c_fc(x))
assert_type(h, Tensor[B, T, 4 * NEmbedding])
return self.c_proj(h)
```
Without config parameterization, each module would need to independently
accept and thread every dimension through its constructor — error-prone and
verbose.
### `Final` class attributes for constants
When a model has fixed hyperparameters, `Final` class attributes let
`Literal` arithmetic work at the type level:
```python
class DCGAN:
nc: Final = 3
nz: Final = 100
ngf: Final = 64
ndf: Final = 64
```
The type checker resolves `DCGAN.ngf * 8` to `Literal[512]`, so you can
write:
```python
self.project = nn.ConvTranspose2d(DCGAN.nz, DCGAN.ngf * 8, 4, 1, 0)
# Type checker infers: ConvTranspose2d[100, 512, ...]
```
## Dynamic construction patterns
Several common patterns break shape tracking. Here's a quick reference and
the fix for each:
### `nn.Sequential(*list_var)`
When modules are constructed in a loop and passed as `*list_var`, the
Sequential loses all type information:
```python
# Bad: Sequential erases module types
layers = [nn.Linear(dim, dim) for _ in range(n)]
self.net = nn.Sequential(*layers) # returns bare Tensor
```
Fix: extract shape-changing modules as individual attributes and chain them
in `forward`. Shape-preserving modules (activations, norms, dropout) can
remain grouped:
```python
# Good: individual attributes preserve types
self.fc1 = nn.Linear(dim, hidden)
self.fc2 = nn.Linear(hidden, dim)
def forward[B](self, x: Tensor[B, D]) -> Tensor[B, D]:
h = F.relu(self.fc1(x))
return self.fc2(h)
```
Note: `nn.Sequential(M1(), M2(), M3())` with direct arguments **is**
tracked — only the `*list_var` form loses types.
### Factory functions returning `nn.Sequential`
Returning `nn.Sequential` from a function erases all type parameters at the
function boundary:
```python
# Bad: factory function — types erased
def _make_block(in_c, out_c) -> nn.Sequential:
return nn.Sequential(nn.Conv2d(in_c, 128, ...), nn.Conv2d(128, out_c, ...))
```
Fix: use a class with a typed `forward` method:
```python
# Good: class preserves shape contract
class Block[InC, OutC](nn.Module):
def __init__(self, in_c: Dim[InC], out_c: Dim[OutC]) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(in_c, 128, ...), nn.Conv2d(128, out_c, ...)
)
def forward[B, H, W](
self, x: Tensor[B, InC, H, W]
) -> Tensor[B, OutC, H, W]:
return self.net(x)
```
### Dimensions from `list[int]`
`list[int]` element access returns `int`, losing the concrete value:
```python
hidden_units = [512, 256, 128]
last_dim = hidden_units[-1] # type is int, not Dim[128]
```
Fix: add an explicit `Dim` field to the config:
```python
@dataclass
class Config[K, MlpOut]:
num_output_features: Dim[K]
mlp_output_dim: Dim[MlpOut] # explicit — was hidden_units[-1]
mlp_hidden_units: list[int] = field(default_factory=lambda: [512, 256])
```
## Typed interfaces
When none of the above restructurings can recover shape tracking, the last
resort is a **typed interface**: the module's `forward` signature provides
the shape contract, and `type: ignore` narrows the internal result:
```python
class DynamicMLP[InDim, OutDim](nn.Module):
def __init__(self, in_dim: Dim[InDim], out_dim: Dim[OutDim],
hidden: list[int]) -> None:
super().__init__()
layers = []
prev = in_dim
for h in hidden:
layers.append(nn.Linear(prev, h))
prev = h
layers.append(nn.Linear(prev, out_dim))
self.layers = nn.ModuleList(layers)
def forward[B](self, x: Tensor[B, InDim]) -> Tensor[B, OutDim]:
h = x
for layer in self.layers:
h = layer(h)
result: Tensor[B, OutDim] = h # type: ignore[bad-assignment]
return result
```
The caller sees a clean `Tensor[B, InDim] -> Tensor[B, OutDim]` contract.
The `type: ignore` is localized to the module's internals.
Use typed interfaces only after exhausting all restructuring options —
they're the fallback, not the first move.
## Key concepts
- **Parameterized configs** propagate dimensions across modules without
threading every `Dim` through every constructor.
- **`Final` class attributes** let the checker resolve constants at the type
level.
- **Dynamic construction** (`Sequential(*list)`, factory functions,
`list[int]` access) breaks tracking — restructure or add explicit `Dim`
fields.
- **Typed interfaces** provide a clean shape contract when internals are
dynamic, but should be the last resort.
---
---
title: "Tutorial 3: Complex Architectures"
description: Type encoder-decoder skip connections and recursive chains with exponential shapes.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Tutorial 3: Complex Architectures
[Tutorial 2](./tensor-shapes-tutorial-loops.mdx) covered shape-preserving loops.
This tutorial tackles architectures where shapes change systematically —
encoder-decoder networks with skip connections, and recursive chains where
dimensions grow or shrink exponentially.
## Encoder-decoder with skip connections
Encoder-decoder architectures (UNet, Demucs, Super SloMo) encode the input
to a bottleneck and then decode back, with skip connections between
corresponding encoder and decoder layers.
### The shape pattern
Each encode step transforms `(B, C, H, W)` to `(B, 2C, H', W')` — doubling
channels and shrinking spatial dimensions. Decoding reverses this, using the
skip connection to restore the original shape. The key insight is that each
encode-recurse-decode cycle **preserves the input shape**:
```
encode: (B, C, H, W) → (B, 2C, H', W')
recurse: preserves (B, 2C, H', W')
decode + skip: (B, 2C, H', W') + (B, C, H, W) → (B, C, H, W)
```
### Typing the recursion
This gives a recursive signature where `recurse` takes and returns the same
shape:
```python
class UNet[NChannels, NClasses](nn.Module):
def _encode[B, C, H, W](
self, x: Tensor[B, C, H, W], depth: int
) -> Tensor[B, 2 * C, (H - 2) // 2 + 1, (W - 2) // 2 + 1]:
idx = len(self.downs) - depth
down: Down[C, 2 * C] = self.downs[idx]
return down(x)
def _decode[B, C, H, W](
self,
skip: Tensor[B, C, H, W],
deep: Tensor[B, 2 * C, (H - 2) // 2 + 1, (W - 2) // 2 + 1],
depth: int,
) -> Tensor[B, C, H, W]:
idx = len(self.ups) - depth
up: Up[2 * C, C] = self.ups[idx]
return up(deep, skip)
def recurse[I, B, C, H, W](
self, x: Tensor[B, C, H, W], depth: Dim[I]
) -> Tensor[B, C, H, W]:
if depth == 0:
return x
skip = x
encoded = self._encode(x, depth)
middle = self.recurse(encoded, depth - 1)
decoded = self._decode(skip, middle, depth)
return decoded
```
### Narrowing annotations for heterogeneous lists
Python has no way to express "element `i` of this list has type
`Stage[C * 2**i]`". The workaround:
1. Declare the list with `Any`: `list[Down[Any, Any]]`
2. Narrow at the access site:
```python
down: Down[C, 2 * C] = self.downs[idx]
```
The `Any` erases element-level type info, and the annotation re-introduces
it for each use.
### Algebraic gaps
Some algebraic equivalences can't be automatically proven. For example,
`((H - 2) // 2 + 1) * 2` does not simplify back to `H`. When you hit this,
use `type: ignore` with a comment explaining the gap:
```python
return up(deep, skip) # type: ignore[bad-argument-type] # ((H-2)//2+1)*2 = H
```
Keep these to an absolute minimum and document each one.
## Recursive chains with exponential shapes
When each stage doubles or halves a dimension, the result after `I` stages
involves `2**I`. This appears in DCGAN (generator and discriminator), ResNet,
and DenseNet.
### The `@overload` pattern
Use `@overload` to separate the base case from the recursive case:
```python
class Generator(nn.Module):
def _apply_stage[B, C, H, W](
self, x: Tensor[B, C, H, W], depth: int
) -> Tensor[B, C // 2, (H - 1) * 2 + 2, (W - 1) * 2 + 2]:
idx = len(self.up_stages) - depth
stage: GenUpStage[C] = self.up_stages[idx]
return stage(x)
@overload
def _chain[B, C, H, W](
self, x: Tensor[B, C, H, W], depth: Dim[1]
) -> Tensor[B, C // 2, H * 2, W * 2]: ...
@overload
def _chain[I, B, C, H, W](
self, x: Tensor[B, C, H, W], depth: Dim[I]
) -> Tensor[B, C // 2 ** I, H * 2 ** I, W * 2 ** I]: ...
def _chain[I, B, C, H, W](
self, x: Tensor[B, C, H, W], depth: Dim[I]
) -> (Tensor[B, C // 2, H * 2, W * 2]
| Tensor[B, C // 2 ** I, H * 2 ** I, W * 2 ** I]):
y = self._apply_stage(x, depth)
if depth == 1:
return y
return self._chain(y, depth - 1)
```
The base-case overload (`depth: Dim[1]`) handles the single-stage case
where the formula simplifies concretely. The recursive overload uses `2**I`
to express the exponential relationship.
### The two-method pattern
This `_apply_stage` + `_chain` pattern separates concerns:
- **`_apply_stage`**: applies a single stage from the `ModuleList`, using a
narrowing annotation to type the list element.
- **`_chain`**: recursively applies `_apply_stage` with overloaded return
types.
The caller invokes `_chain` with a concrete depth:
```python
def forward[B](self, input: Tensor[B, 100, 1, 1]) -> Tensor[B, 3, 64, 64]:
h0 = F.relu(self.project_bn(self.project(input)))
assert_type(h0, Tensor[B, 512, 4, 4])
h1 = self._chain(h0, 3) # 512->64, 4->32
assert_type(h1, Tensor[B, 64, 32, 32])
return torch.tanh(self.output(h1))
```
## Key concepts
- **Recursive shape preservation**: encode-recurse-decode cycles preserve
the input shape, enabling a clean recursive signature.
- **Narrowing annotations** re-introduce type information lost by
heterogeneous `ModuleList`s.
- **`@overload`** separates base and recursive cases for exponential shape
chains.
- **`type: ignore`** is a last resort for algebraic gaps the checker can't
prove. Always document the specific equivalence.
## Next steps
In [Tutorial 4](./tensor-shapes-tutorial-advanced.mdx), you'll see how to handle
config classes with type parameters, dynamic construction patterns, and
other advanced techniques.
---
---
title: "Tutorial 1: Your First Port"
description: Learn the basics of tensor shape annotations by typing a simple multi-layer perceptron (MLP) model.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Tutorial 1: Your First Port
In this tutorial, you'll add tensor shape annotations to a simple multi-layer perceptron (MLP) model.
By the end, you'll understand `Dim`, `Tensor[...]`, class-level type
parameters, and method-level type parameters.
```sandbox
dir: tensor-shapes-tutorial-basics
description: Try porting a simple MLP with class-level and method-level type parameters.
```
## The model
Here's a simple actor network from a reinforcement learning setup — three
Linear layers in sequence:
```python
class BaselineActor(nn.Module):
def __init__(self, state_size: int, action_size: int):
super().__init__()
self.fc1 = nn.Linear(state_size, 400)
self.fc2 = nn.Linear(400, 400)
self.out = nn.Linear(400, action_size)
def forward(self, state):
h1 = F.relu(self.fc1(state))
h2 = F.relu(self.fc2(h1))
return torch.tanh(self.out(h2))
```
Without shape annotations, every intermediate value is just `Tensor`. You
can't tell from reading the code what shape `h1` has, or whether the layer
dimensions are consistent.
## Step 1: Identify the dimensions
The constructor takes two parameters that determine tensor dimensions:
- `state_size` — the input dimension (flows into `nn.Linear`)
- `action_size` — the output dimension (flows into `nn.Linear`)
Both flow to sub-module constructors, so both must be `Dim`, not `int`.
(`Dim[X]` is a type that bridges a runtime integer value to a type-level
symbol `X` — see [Getting Started](./tensor-shapes-setup.mdx) for details.)
There are also two fixed constants: `400` (hidden dimension). These are
literal values, not parameters, so they don't need type params.
## Step 2: Type the constructor
Make the dimension parameters into `Dim[...]` and add class-level type
parameters:
```python
class BaselineActor[S, A](nn.Module):
def __init__(self, state_size: Dim[S], action_size: Dim[A]) -> None:
super().__init__()
self.fc1 = nn.Linear(state_size, 400)
self.fc2 = nn.Linear(400, 400)
self.out = nn.Linear(400, action_size)
```
Now when someone writes `BaselineActor(24, 4)`, the type checker binds
`S = 24` and `A = 4`, inferring the type `BaselineActor[24, 4]`. The
sub-modules are automatically typed: `self.fc1` is `Linear[24, 400]`,
`self.out` is `Linear[400, 4]`.
## Step 3: Type the forward
The forward method has one dynamic dimension — **batch size** — that
varies across calls. Make it a method-level type parameter:
```python
def forward[B](self, state: Tensor[B, S]) -> Tensor[B, A]:
h1 = F.relu(self.fc1(state))
h2 = F.relu(self.fc2(h1))
return torch.tanh(self.out(h2))
```
`S` and `A` are class-level params (fixed at construction). `B` is a
method-level param (bound per call).
## Step 4: Verify inferred shapes
Add `assert_type` after each intermediate to verify what pyrefly infers:
```python
def forward[B](self, state: Tensor[B, S]) -> Tensor[B, A]:
h1 = F.relu(self.fc1(state))
assert_type(h1, Tensor[B, 400])
h2 = F.relu(self.fc2(h1))
assert_type(h2, Tensor[B, 400])
act = torch.tanh(self.out(h2))
assert_type(act, Tensor[B, A])
return act
```
Run `pyrefly check`. If any `assert_type` fails, the shape you expected
doesn't match what pyrefly inferred — investigate the mismatch.
Once all shapes check out, **remove the `assert_type` calls**. Each one
corresponds to an inlay type hint that your IDE shows permanently. Pyrefly
catches shape errors through your function signatures and return types
regardless — you don't need `assert_type` in the final code.
## Step 5: Add a smoke test
Smoke tests exercise the model at concrete dimensions. They verify that
the shape annotations are consistent end-to-end:
```python
def test_baseline_actor():
actor = BaselineActor(24, 4)
state = torch.randn(8, 24)
# pyrefly infers: Tensor[8, 24]
act = actor(state)
# pyrefly infers: Tensor[8, 4]
```
Use concrete dimensions in tests (`Tensor[8, 24]`, not generic `Tensor[B, S]`)
so the type checker verifies the full shape calculation.
## The complete port
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```python
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
from shape_extensions import Dim
class BaselineActor[S, A](nn.Module):
def __init__(self, state_size: Dim[S], action_size: Dim[A]) -> None:
super().__init__()
self.fc1 = nn.Linear(state_size, 400)
self.fc2 = nn.Linear(400, 400)
self.out = nn.Linear(400, action_size)
def forward[B](self, state: Tensor[B, S]) -> Tensor[B, A]:
h1 = F.relu(self.fc1(state))
# pyrefly infers: Tensor[B, 400]
h2 = F.relu(self.fc2(h1))
# pyrefly infers: Tensor[B, 400]
act = torch.tanh(self.out(h2))
# pyrefly infers: Tensor[B, A]
return act
```
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
class BaselineActor(nn.Module):
def __init__(self, state_size: int, action_size: int):
super().__init__()
self.fc1 = nn.Linear(state_size, 400)
self.fc2 = nn.Linear(400, 400)
self.out = nn.Linear(400, action_size)
def forward(self, state):
h1 = F.relu(self.fc1(state))
# what shape is h1? Tensor — that's all you know
h2 = F.relu(self.fc2(h1))
return torch.tanh(self.out(h2))
```
:::note
This example uses `from __future__ import annotations` with new-style
generics — the simplest setup. Pyrefly shows inferred shapes as inlay type
hints in your IDE. If you want `assert_type` for runtime regression guards,
see [Getting Started](./tensor-shapes-setup.mdx#option-2-shape_extensionstypevar-runtime-compatible)
for the `shape_extensions.TypeVar` import style, which requires old-style
generics.
:::
## Key concepts
- **`Dim[X]`** bridges runtime integer values to type-level symbols. Constructor
parameters that determine tensor dimensions should be `Dim[X]`, not `int`.
- **Class type parameters** (`class Foo[S, A]`) represent dimensions fixed at
construction time.
- **Method type parameters** (`def forward[B]`) represent dimensions that vary
per call (batch size, sequence length).
- **Inlay type hints** show inferred shapes in your IDE. Use `reveal_type`
during development to inspect shapes in checker output.
## Next steps
This model had a simple linear pipeline — each layer feeds into the next with
known shapes. In [Tutorial 2](./tensor-shapes-tutorial-loops.mdx), you'll see what
happens when layers are stacked in loops, as in Transformer architectures.
---
---
title: "Tutorial 2: Loops and Stacking"
description: Type shape-preserving loops and ModuleList iteration in Transformer-style architectures.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Tutorial 2: Loops and Stacking
In [Tutorial 1](./tensor-shapes-tutorial-basics.mdx), every layer was called
directly. But Transformer-style architectures stack identical layers in a
`ModuleList` and iterate over them. This tutorial shows how to type those
patterns.
## Shape-preserving loops
When every layer in a loop has the same type signature — input shape equals
output shape — the type checker can verify that the loop preserves the shape
invariant.
Here's a Transformer encoder that stacks `n_layers` identical
`EncoderLayer` modules:
```python
class Encoder[NHead, DK, DInner](nn.Module):
def __init__(
self,
n_head: Dim[NHead],
d_k: Dim[DK],
d_inner: Dim[DInner],
n_layers: int = 6,
) -> None:
super().__init__()
self.layer_stack = nn.ModuleList(
[EncoderLayer(n_head, d_k, d_inner) for _ in range(n_layers)]
)
def forward[B, T](
self, src_seq: Tensor[B, T, NHead * DK]
) -> Tensor[B, T, NHead * DK]:
enc_output = src_seq
for layer in self.layer_stack:
enc_output, _attn = layer(enc_output)
assert_type(enc_output, Tensor[B, T, NHead * DK])
return enc_output
```
Notice that `n_layers` is `int`, not `Dim` — it's an iteration count that
doesn't flow into any tensor dimension. Only values that determine tensor
shapes need to be `Dim`.
Each `EncoderLayer` takes `Tensor[B, T, NHead * DK]` and returns the same
shape, so the loop preserves the invariant and the type checker is
satisfied.
### Derived dimensions
The model dimension here is `NHead * DK` — a derived expression, not an
independent type parameter. This is important: only independent degrees of
freedom get type params. If you wrote `class Encoder[NHead, DK, D]` with
an independent `D`, you'd lose the constraint that `D == NHead * DK`.
## Separating the first iteration
Sometimes the first iteration of a loop changes the shape, while subsequent
iterations preserve it. The type checker sees the union of both shapes and
widens to a less precise type.
The fix is to separate the first iteration:
```python
# Problem: x widens to Tensor[B, F, D] | Tensor[B, K, D]
x = input_embs
for layer in self.layers:
x = layer(x)
out: Tensor[B, K, D] = x # type: ignore[bad-assignment]
# Solution: no union, no type: ignore
x = self.layers[0](input_embs) # [B, F, D] -> [B, K, D]
assert_type(x, Tensor[B, K, D])
for i in range(1, len(self.layers)):
x = self.layers[i](x) # [B, K, D] -> [B, K, D]
```
The first call changes the shape; subsequent calls preserve it. By
separating them, you avoid the union widening entirely.
## Shape-preserving activations
Many architectures accept an activation function as a parameter (ReLU, GELU,
SiLU, etc.). Since each activation's forward signature is
`Tensor[*S] -> Tensor[*S]`, you can express this with a type alias:
```python
ShapePreservingActivation = (
type[nn.ReLU] | type[nn.GELU] | type[nn.SiLU] | type[nn.Tanh]
)
class ResBlock[C](nn.Module):
def __init__(self, c: Dim[C], act_fn: ShapePreservingActivation) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(c, c, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(c),
act_fn(),
)
```
The `Sequential` chains the modules and the type checker verifies that the
overall shape is preserved.
## Multi-head attention
Multi-head attention involves reshaping tensors from `[B, T, D]` to
`[B, NHead, T, D // NHead]` and back. The dimension arithmetic is expressed
directly in annotations:
```python
class CausalSelfAttention[NEmbedding, NHead](nn.Module):
def __init__(
self,
n_embd: Dim[NEmbedding],
n_head: Dim[NHead],
) -> None:
super().__init__()
self.c_attn = nn.Linear(n_embd, 3 * n_embd)
self.c_proj = nn.Linear(n_embd, n_embd)
self.n_head = n_head
self.n_embd = n_embd
def forward[B, T](
self, x: Tensor[B, T, NEmbedding]
) -> Tensor[B, T, NEmbedding]:
qkv = self.c_attn(x)
assert_type(qkv, Tensor[B, T, 3 * NEmbedding])
q, k, v = qkv.split(self.n_embd, dim=2)
assert_type(q, Tensor[B, T, NEmbedding])
# Reshape for multi-head: [B, T, D] -> [B, NHead, T, D // NHead]
head_dim = self.n_embd // self.n_head
q = q.view(q.size(0), q.size(1), self.n_head, head_dim)
q = q.transpose(1, 2)
assert_type(q, Tensor[B, NHead, T, NEmbedding // NHead])
...
```
The type checker tracks the reshape and transpose, verifying that
`D // NHead` is consistent throughout the attention computation.
## Key concepts
- **`int` for iteration counts, `Dim` for tensor dimensions.** `n_layers`
doesn't flow into tensor shapes, so it stays `int`.
- **Derived dimensions** express relationships: `NHead * DK`, not
independent `D`.
- **Separate the first iteration** when it changes shape to avoid union
widening.
- **Arithmetic in annotations** (`3 * NEmbedding`, `NEmbedding // NHead`)
is tracked and simplified automatically.
## Next steps
In [Tutorial 3](./tensor-shapes-tutorial-architectures.mdx), you'll see how to
handle encoder-decoder architectures with skip connections, where shapes
change as you go deeper and must be restored on the way back up.
---
---
title: Tensor Shapes
description: An overview of Pyrefly's tensor shape type system for static type checking of PyTorch models.
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
# Tensor Shapes
:::info
The support described here will be in the *upcoming* Pyrefly version 1.1.0. Experimental tensor shape support in 1.0.0 was more limited and is not recommended.
:::
:::warning Experimental
This feature is experimental. The API and behavior may change in future releases without notice.
::::
Pyrefly can track **tensor shapes** through your PyTorch models, giving you
**end-to-end static type checking** of shape transformations.
## Why tensor shapes?
When you write PyTorch code, the hardest thing to keep track of is tensor
shapes. Every operator transforms shapes in non-trivial ways, and shape
mistakes don't always crash — they can silently produce wrong results. With
Pyrefly's tensor shape support, you get **automatic inlay type hints showing
tensor shapes as you type**, so you can see exactly what shape every
intermediate tensor has without running your code or adding print statements.
Here's the same NanoGPT forward method, without and with tensor shape
tracking:
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
With tensor shapes enabled, Pyrefly infers and displays the shape of every
intermediate tensor — `Tensor[B, T, NEmbedding]` for embeddings,
`Tensor[T]` for position indices — without any manual annotations on local
variables.
```sandbox
dir: tensor-shapes-overview
description: See tensor shape tracking, variadic batch dimensions, and shape mismatch errors in action.
```
## How it works
Pyrefly's tensor shape support is built on two extensions that work together:
1. **Symbolic integer arithmetic** in the core type system — lets you write
`Tensor[B, C, H, W]` and have arithmetic like `D // NHead` work at the
type level.
2. **Shape transform specifications** for PyTorch operators — a library of
shape rules that tells Pyrefly how each operator transforms shapes.
With these two extensions, you can use Pyrefly to typecheck real-world PyTorch
models, where the tensor shapes of all local variables are inferred from just
a few annotations at class and function boundaries.
### Symbolic integer arithmetic
You can write tensor types with integer dimensions — `Tensor[3, 4]` is a
2D tensor with shape `(3, 4)`. This works for modules too: `nn.Linear[3, 4]`
takes `Tensor[..., 3]` as input and returns `Tensor[..., 4]`.
**`Dim[X]`** bridges runtime integer values to the type level. When
`x: Tensor[3, 4]`, then `x.shape` has type `tuple[Dim[3], Dim[4]]` — you
can extract dimensions from tensors and use them to construct new ones.
Arithmetic works too: if `a: Dim[3]` and `b: Dim[4]`, then
`a * b: Dim[12]`.
**Generic type parameters** let you write shape-polymorphic modules:
```python
class Linear[N, M]:
def __init__(self, n: Dim[N], m: Dim[M]):
...
def forward[*Xs](self, inp: Tensor[*Xs, N]) -> Tensor[*Xs, M]:
...
linear: Linear[3, 4] = Linear(3, 4)
inp: Tensor[2, 5, 3] = ...
x: Tensor[2, 5, 4] = linear(inp)
```
`Dim` encodes symbolic shapes at the type level throughout PyTorch, covering modules as well as tensors.
**Arithmetic on type variables** lets you write custom shape transforms:
```python
def custom_rand_tensor[A, B](a: Dim[A], b: Dim[B]) -> Tensor[(A + B) // 2]:
return torch.randn((a + b) // 2)
x: Tensor[3] = custom_rand_tensor(2, 4)
```
While these examples use type annotations for exposition, the types of local
variables like `x` and `linear` are *inferred automatically* — Pyrefly shows
them as inlay type hints in your editor.
### Shape transform specifications
Some PyTorch operators have simple shape signatures that can be expressed as
standard type stubs. For example, `torch.mm`:
```python
def mm[M, K, N](x: Tensor[M, K], y: Tensor[K, N]) -> Tensor[M, N]:
...
```
For operators with more complex shape logic (like `reshape`, `cat`, or
`F.interpolate`), Pyrefly uses a small DSL to specify shape transforms:
```python
# Internal library definition — not user-facing code
def repeat_ir(self: Tensor, sizes: list[int | symint]) -> Tensor:
return Tensor(shape=[d * r for d, r in zip(self.shape, sizes)])
```
This means you can extend Pyrefly's shape coverage for new PyTorch operators
without touching Pyrefly's internals — see the
[contributing guide](./tensor-shapes-contributing.mdx) for details.
## Related Work
Tensor shape checking is not a new idea. Several projects have explored this
space, each with different trade-offs. Here's how Pyrefly's approach compares.
### Pyre and Pyright (static type checking)
An early attempt at tensor shapes in Pyre, the precursor of Pyrefly, is
described in [this PyCon 2021 talk](https://www.youtube.com/watch?v=ld9rwCvGdhc&t=11470s).
That system used `Literal` to wrap concrete sizes and explicit type constructors
like `IntDiv` for arithmetic — avoiding the need to support integers as type
arguments or arithmetic on type variables, but at the cost of verbose syntax
(e.g., `Tensor[float, M, Literal[2], IntDiv[M, 2]]`).
More recently, [this video](https://www.youtube.com/watch?v=gJRkJVs3GAI) details
two approaches to extend Pyright with tensor shapes, with the author concluding
neither was the right approach. Both suffered from verbose syntax and a large set
of type-level operators.
Pyrefly's system is leaner by design:
- No constraint solver — Pyrefly prioritizes "more shape inference with fewer
annotations" rather than "more red squiggles with more annotations."
- The complexity of specifying shape transforms for PyTorch operators is
separated into a DSL available to library maintainers, keeping the type
system features available to users simple.
### Jaxtyping (runtime type checking)
With [jaxtyping](https://github.com/patrick-kidger/jaxtyping), users can
express tensor shapes in type annotations that other libraries like `typeguard`
and `beartype` can check at runtime. The syntax is designed to be universal for
array-like containers (supporting JAX, NumPy, and PyTorch), but is somewhat
verbose — for example, `Shaped[Tensor, "M 2 M//2"]` instead of
`Tensor[M, 2, M // 2]`.
Pyrefly supports jaxtyping annotations as an alternative front-end to our native
syntax; these annotations are translated internally to use generics and display
back in jaxtyping syntax.
However, there is a significant limitation of jaxtyping: there is no way to
share symbolic dimensions across variables and functions in a class (see
[this issue](https://github.com/patrick-kidger/jaxtyping/issues/383)). This
limits jaxtyping to individual functions that operate on tensors, rather than a
hierarchy of modules connecting them end-to-end. In practical terms, our fully
typechecked implementations of real-world models (e.g., NanoGPT) cannot be
faithfully ported to use jaxtyping syntax alone.
## Get involved
Tensor shapes are under active development. We welcome contributions —
especially new fixture stubs and DSL specifications for PyTorch operators.
See the [contributing guide](./tensor-shapes-contributing.mdx) to get started.
Join the conversation:
- [Pyrefly Discord](https://discord.gg/BVWQdkkEFJ)
- [Python typing Discourse](https://discuss.python.org/c/typing/32)
- [GitHub Issues](https://github.com/facebook/pyrefly/issues)
---
---
title: Typing for Python Developers
description: Get to know Python's Type System with working examples
---
{/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/}
import CodeSnippet from '@site/src/sandbox/CodeSnippet'
# Typing for Python Developers
*A 5‑Minute Tour with Pyrefly.*
**Goal:** In five minutes you'll know how Python's static type system _infers_, _defines_, and _composes_ types—and you'll have copy‑paste snippets to start using right away.
If you are new to Python typing, check out our [Python Typing 101 guide](../python-typing-for-beginners/).
Python's type system allows you to annotate variables so you, your teammates and your type checker can find bugs before you run your code. Think of it as documentation that's automatically validated and will help your IDE help you.
_TL;DR_
* Catch bugs **before** running the code.
* Improve editor autocomplete & refactors.
* Turn your code into living documentation.
### Types with Inference
Static analyzers can often _infer_ types from your code—no annotations required. Pyrefly takes this a step further.
### Where Inference Shines ✨
- Constant assignments (`answer = 42 -> int`)
- List/tuple/dict literals with uniform elements (`names = ["A", "B"] -> list[str]`)
- Return types if parameter types are annotated:
int
`}
/>
### When to Add Hints
- Public APIs (library or service boundaries)
- Mixed collections (`list[int | str]`)
- Callable signatures (decorators, callbacks)
## Define Types Inline
### The Basics
Python's built-in types can be used to write many type hints.
### Functions
Defining the parameter and return types for a function doesn't just help prevent bugs, but it makes it easier to navigate in other files. You don't always need to define a return type - we'll do our best to infer it for you! We can't always get it right and an explicit return type will help your IDE navigate faster and more accurately.
str:
return f"Hello, {name}!"
greet("Pyrefly")
def whatDoesThisFunctionReturnAgain(a: int, b: int):
return a + b
reveal_type(whatDoesThisFunctionReturnAgain(2, 3)) # revealed type: int
`}
/>
## Advanced Types
### Composing Types
The real power comes from composing smaller pieces into richer shapes.
### Unions & Optional
Optional[int]:
if data is None:
return None
if isinstance(data, bytes):
data = data.decode()
return int(data)
`}
/>
### Generics
Generics allow you to define reusable functions and classes that work with multiple types. This feature enables you to write more flexible and adaptable code.
**Declaring Generic Classes:**
list[T]:
return [self.x]
c = C(0)
reveal_type(c.box()) # revealed type: list[int]
`}
/>
**Declaring Type Statements:**
**ParamSpec and TypeVarTuple:**
### Variance Inference in Generics
When working with generics, a key question is: if one type is a subtype of another, does the subtyping relationship carry over to generic types?
For example, if `int` is a subtype of `float`, is `A[int]` also a subtype of `A[float]`?
This behavior is governed by variance:
- Covariant types preserve the direction of subtyping (`A[int]` is a subtype of `A[float]`).
- Contravariant types reverse it.
- Invariant types require an exact match.
Before [PEP 695](https://peps.python.org/pep-0695/), variance had to be declared manually and was often confusing.
Pyrefly infers the variance automatically based on how each type parameter is used - in method arguments, return values, attributes, and base classes.
**Example 1:** Covariance from Immutable Attributes (`Final`)
**How Variance is Inferred:**
- The attribute `x` is annotated as `Final[T]`, making it immutable after initialization.
- Because `T` appears only in this read-only position, it is safe to infer `T` as covariant.
- This means:
- You can assign `ShouldBeCovariant[int]` to a variable expecting `ShouldBeCovariant[float]` (since `int` is a subtype of `float`).
- But the reverse is not allowed (`ShouldBeCovariant[float]` to `ShouldBeCovariant[int]`), which triggers a type error.
**Example 2:** General Variance Inference from Method and Base Class Usage
None:
...
def method2(self) -> T3:
...
def func_a(p1: ClassA[float, int, int], p2: ClassA[int, float, float]):
v1: ClassA[int, int, int] = p1 # ERROR!
v2: ClassA[float, float, int] = p1 # ERROR!
v3: ClassA[float, int, float] = p1 # OK
v4: ClassA[int, int, int] = p2 # ERROR!
v5: ClassA[int, int, float] = p2 # OK
`}
/>
**How Variance is Inferred:**
- `T1` appears in the base class `list[T1]`. Since list is mutable, `T1` is invariant.
- `T2` is used as the type of a method parameter (`a: T2`) so `T2` contravariant.
- `T3` is the return type of a method (`def method2() -> T3`) so `T3` is covariant.
- This means:
- `v1` fails due to mismatched `T1` (invariant).
- `v2` fails because `T2` expects a supertype, but gets a subtype.
- `v4` fails because `T3` expects a subtype, but gets a supertype.
## Structural Types and Protocols
Python also employs a structural type system, often referred to as "duck typing." This concept is based on the idea that if two objects have the same shape or attributes, they can be treated as being of the same type.
### Dataclasses
Dataclasses allow you to create type-safe data structures while minimizing boilerplate.
### TypedDict
Typed dictionaries enable you to define dictionaries with specific key-value types. This feature lets you bring type safety to ad-hoc dictionary structures without major refactoring.
### Overloads
Overloads allow you to define multiple function signatures for a single function. Like generics, this feature helps you write more flexible and adaptable code.
int: ...
@overload
def f(x: str) -> str: ...
def f(x: int | str) -> int | str:
return x
reveal_type(f(0)) # revealed type: int
reveal_type(f("")) # revealed type: str
`}
/>
### Protocols
Protocols allows you to define interfaces without explicit inheritance. This feature helps you write more modular and composable code.
None: ...
class GoodWorld:
def write(self) -> None:
print("Hello world!")
class BadWorld:
pass
def f(writer: Writer):
pass
f(GoodWorld()) # OK
f(BadWorld()) # ERROR!
`}
/>
## Typing Features and PEPS available in each Python Version
See the full list of features available in the Python type system [here](../python-features-and-peps).
### Key Highlights Summary:
- **Inference:** Python's static analyzers can infer types from your code, reducing the need for explicit annotations. This feature enhances code readability and helps catch bugs early.
- **Defining Types:** You can define types inline using Python's built-in types, which aids in documentation and improves IDE support.
- **Advanced Types:** The guide covers advanced concepts like composing types, using unions and optionals, generics, protocols, and structural types like dataclasses and TypedDict.
- **Practical Examples:** The guide includes examples of functions, generic classes, structural typing with protocols, and more, demonstrating how to apply these concepts in real-world scenarios.