_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.
Coming from another type checker? See the
[mypy error-code mapping](./migrate/mypy/error-codes.mdx) and the
[Pyright diagnostic mapping](./migrate/pyright/diagnostics-reference.mdx) for how those
diagnostics correspond to the kinds below.
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-dataclass-descriptor
A dataclass field is typed as a descriptor whose read-back type does not match what the
synthesized `__init__` writes.
A **data descriptor** defines `__set__` and/or `__delete__`. It always takes priority over the
instance dictionary, so when one is used as a dataclass field the class-level descriptor acts as
the field's default. If it also defines `__get__`, reading the field yields the `__get__` return
type while writing it goes through `__set__`, so the two must agree:
```python
from dataclasses import dataclass
class Desc:
def __get__(self, obj, cls) -> int: ...
def __set__(self, obj, value: str) -> None: ...
@dataclass
class C:
x: Desc = Desc() # `__get__` returns `int`, but `__set__` accepts `str` [bad-dataclass-descriptor]
```
A **non-data descriptor** defines `__get__` but neither `__set__` nor `__delete__`. It does not
take priority over the instance dictionary, so the synthesized `__init__` writes straight into
that dictionary and shadows the class-level descriptor. Every later read returns the raw value
rather than calling `__get__`:
```python
from dataclasses import dataclass
class Desc:
def __get__(self, obj, cls) -> int: ...
@dataclass
class C:
x: Desc = Desc() # instance attribute shadows the descriptor [bad-dataclass-descriptor]
```
A non-data descriptor whose `__get__` returns `Self` or its own class is exempt, since the
shadowing value then has the same type as the read-back value.
## 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-singledispatch-register
A `functools.singledispatch` implementation is registered with a dispatch type that is not a subtype of the fallback function's first parameter. Since that parameter defines the function's declared domain, the registration is outside it and can never be dispatched to through type-correct calls (often meaning the fallback was annotated too narrowly).
```python
from functools import singledispatch
@singledispatch
def f(arg: int) -> None: ...
@f.register
def _(arg: str) -> None: ... # bad-singledispatch-register: `str` is not a subtype of `int`
```
## 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()
```
## column-schema-mismatch
A Polars DataFrame's data columns do not name exactly the columns declared by its `schema=`. A declared `schema` is the authoritative column set, so a data column that is missing from it or absent from the data raises `ValueError` or `KeyError` at runtime.
```python
import polars as pl
pl.DataFrame({"x": [1]}, schema={"a": pl.Int64}) # data column `x` is not in the schema, and `a` has no data
```
## column-type-mismatch
A Polars DataFrame column literal has an element whose type does not fit the column's dtype. Polars infers the dtype from the first element and requires every later element to fit it, so a mismatch raises `TypeError` at runtime.
```python
import polars as pl
pl.DataFrame({"a": [1, 2.0]}) # column `a` is `int`, so the `float` does not fit
```
## 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!
```
## direct-abstract-base-instantiation
Default severity: `warn`
This diagnostic is raised when code instantiates a class that directly extends
`abc.ABC` or directly uses `abc.ABCMeta`, even if the class has no abstract
methods. Python permits such classes to be instantiated, and some libraries use
`ABC` without prohibiting construction.
## 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
```
## duplicate-column
A Polars projection produces more than one column with the same name. Polars rejects duplicate output names when an eager projection runs or a lazy projection is collected.
```python
import polars as pl
df = pl.DataFrame({"a": [1]})
df.select("a", "a")
```
## empty-body
Default severity: `ignore`
A function body consists only of `...` even though the function declares a
return type that `None` is not assignable to.
Empty ellipsis bodies are allowed in stub files, protocol methods, abstract
methods, overload declarations, and `if TYPE_CHECKING` blocks.
```python
def f() -> int: ... # error: empty body
```
## 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-lambda`](#implicit-any-lambda)
- [`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-lambda
Default severity: `ignore`
A lambda parameter whose type cannot be inferred is treated as an implicit `Any`, hiding type errors in the lambda body and at its call sites.
When the lambda is used in a typed context (e.g. assigned to a `Callable` or passed to a function that expects one), the parameters are inferred from that context and no error is reported.
```python
f = lambda x: x # error: implicit-any-lambda
from typing import Callable
g: Callable[[int], int] = lambda x: x # OK: `x` is inferred as `int` from context
```
## 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.
```python
def f(x):
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-bool
Default severity: `ignore`
This error is emitted when a non-`bool` value is used in a boolean context. Enable it to require
explicit checks for values whose truthiness could conflate distinct cases, such as `None` and zero
or `None` and an empty container. An explicit `bool(...)` conversion is allowed.
```python
def get_temperature() -> int | None:
return 0
temperature = get_temperature()
if not temperature: # implicit-bool
raise RuntimeError("Temperature sensor unavailable")
# Fix:
if temperature is None:
raise RuntimeError("Temperature sensor unavailable")
```
## 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(...)
```
## implicit-reexport
Default severity: `ignore`
This error is emitted when you import a name from a module that only made it
available through a plain `import` or `from ... import ...` statement. Per the
[typing spec](https://typing.python.org/en/latest/spec/distributing.html#import-conventions),
such a name is not part of the module's public interface, so relying on it is
fragile: the intermediate module may stop importing it at any time.
A module explicitly re-exports a name when it is redundantly aliased
(`from x import y as y` or `import x as x`), listed in `__all__`, or brought in
via a wildcard (`from x import *`).
```python
# foo.py
a = 1
# bar.py
from foo import a # `a` is not re-exported from `bar`
# baz.py
from bar import a # error: `a` is not exported from module `bar`
# Fix in bar.py:
from foo import a as a # or add `a` to `__all__`
```
## 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-abstract-method
Default severity: `ignore`
Pyrefly emits this error when a class that is not abstract (does not inherit from `abc.ABC`, use `abc.ABCMeta`, have any transitive abstract base, or define a `Protocol` or `NewType`) defines a method decorated with `@abstractmethod`. Such a class is directly instantiable at runtime, yet contains an unimplemented method — a likely programming mistake.
```python
from abc import abstractmethod
class Foo:
@abstractmethod # Error: `Foo.fn` is decorated with `@abstractmethod` but `Foo` is not an abstract class
def fn(self) -> int: ...
```
To fix the issue, either inherit from `abc.ABC` to make the class abstract, define a `Protocol` if the class is an interface, or remove the `@abstractmethod` decorator and provide a concrete implementation.
## 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-cast
Default severity: `ignore`
This warning is raised when `typing.cast` or `typing_extensions.cast` converts
between types that Pyrefly can prove have disjoint runtime classes. The check
applies to class instances and objects, literals, `None`, tuples, `TypedDict`,
and unions containing only supported types. Literals and structural containers
are compared using their nominal runtime classes. This means that a
`TypedDict` overlaps with `dict`, and tuple element types do not by themselves
make two tuples disjoint. Generic arguments similarly do not make two class
instances disjoint, so casting `list[int]` to `list[str]` does not produce this
warning. Type variables, protocols, `Never`, callables, and other types without
a sufficiently precise nominal runtime class are not reported. Two open nominal
classes are not considered disjoint unless their disjoint-base representatives
conflict, because a multiple-inheritance subclass could satisfy both.
```python
from typing import cast
x: int = 1
cast(str, x) # Cast from `int` to `str` is invalid because the types are disjoint [invalid-cast]
```
## 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 where it is not allowed.
For example, `Self` is not allowed inside a `TypedDict`, so the following code
errors:
```python
from typing import Optional, Self, TypedDict
class TD(TypedDict):
x: Optional[Self] # error: `Self` is not allowed in a `TypedDict`
```
## 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-checking-constant
A module-level `TYPE_CHECKING` constant that is not typed as `bool`. Type checkers treat
`TYPE_CHECKING` as `True`, while at runtime it is `False`, so a user-defined one must be a `bool`
(conventionally `TYPE_CHECKING = False`).
```python
TYPE_CHECKING: str = "" # error: not a bool
```
## 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
```
## misplaced-ignore
Default severity: `warn`
A file-level `# pyrefly: ignore-errors` (or `# pyrefly: ignore-errors[code]`) directive
only takes effect when it appears at the top of the file, before any code. Placed after
the first line of code it is silently inert — it suppresses nothing — so Pyrefly flags it.
Move the directive to the top of the file, or use a line-level `# pyrefly: ignore[code]`
to suppress a single line.
```python
import os
# pyrefly: ignore-errors # inert here — flagged as misplaced-ignore
x: int = "not an int" # this error is still reported
```
## 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-attribute-patch-target
Default severity: `warn`
The attribute targeted by a `unittest.mock.patch` call does not exist. Example:
```python
from unittest import mock
@mock.patch("dep.nonexistent_attr") # missing-attribute-patch-target
def f(): ...
```
This is a sub-kind of [missing-attribute](#missing-attribute): suppressing `missing-attribute` also suppresses 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.
## missing-super-call
Default severity: `ignore`
A constructor-like method overrides a parent class method but does not call `super()`.
```python
class Base:
def __init__(self) -> None:
self.value: int = 1
class Child(Base):
def __init__(self) -> None: # missing-super-call
pass
```
To enable this check, set the severity to `error` in your configuration:
```toml
[tool.pyrefly]
errors = { missing-super-call = "error" }
```
## 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-any-return
Default severity: `ignore`
Umbrella error code for cases where a returned expression is `Any` in a function declared to return a concrete type other than `object`. Enabling this catches places where `Any` silently bypasses the function's declared return type. Returning `Any` as `object` is allowed because every runtime value is an object, so this does not weaken the return type's guarantee.
Concrete diagnostics are emitted under one of the more specific sub-kinds:
- [`no-any-return-explicit`](#no-any-return-explicit)
- [`no-any-return-implicit`](#no-any-return-implicit)
## no-any-return-explicit
Default severity: `ignore`
The returned expression has type `Any`, but the function is declared to return a concrete type other than `object`. The returned `Any` originates from an explicit annotation or other introduction into the type system.
This is a sub-kind of [no-any-return](#no-any-return): suppressing `no-any-return` also suppresses this error.
```python
from typing import Any
def get_port(config: dict[str, Any]) -> int:
return config["port"] # no-any-return-explicit: Returning Any from function declared to return "int"
# Fix: validate the value at the boundary where untyped data enters typed code.
def get_typed_port(config: dict[str, object]) -> int:
port = config["port"]
if isinstance(port, int):
return port
raise ValueError("port must be an int")
```
## no-any-return-implicit
Default severity: `ignore`
The returned expression was inferred as `Any` (e.g., from an untyped import or missing stub), but the function's return annotation is a concrete type other than `object`. This usually indicates that adding type information at an untyped boundary would restore type precision.
This is a sub-kind of [no-any-return](#no-any-return): suppressing `no-any-return` also suppresses this error.
```python
def get_port(config) -> int:
return config["port"] # no-any-return-implicit: Returning implicit Any from function declared to return "int"
# Fix: annotate the parameter and validate the value before returning it.
def get_typed_port(config: dict[str, object]) -> int:
port = config["port"]
if isinstance(port, int):
return port
raise ValueError("port must be an int")
```
## 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` over a closed subject type, such as an enum, literal
union, `None`, `bool`, or non-subclassable class, may fall through without matching
a case. Add the missing cases or a default arm.
Non-exhaustive matches over open subject types are reported under a sub-kind,
[non-exhaustive-match-open-type](#non-exhaustive-match-open-type).
```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"
```
## non-exhaustive-match-open-type
Default severity: `ignore`
Enable this error kind to have Pyrefly report when a `match` over an open
subject type such as `int`, `str`, or `object` is non-exhaustive.
This is a sub-kind of [non-exhaustive-match](#non-exhaustive-match), so
configuring or suppressing the parent kind also applies to this diagnostic.
## 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.).
This is a sub-kind of [pytorch-efficiency-lints](#pytorch-efficiency-lints): enabling `pytorch-efficiency-lints` also enables this error.
```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.
This is a sub-kind of [pytorch-efficiency-lints](#pytorch-efficiency-lints): enabling `pytorch-efficiency-lints` also enables this error.
```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)`.
This is a sub-kind of [pytorch-efficiency-lints](#pytorch-efficiency-lints): enabling `pytorch-efficiency-lints` also enables this error.
```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.
This is a sub-kind of [pytorch-efficiency-lints](#pytorch-efficiency-lints): enabling `pytorch-efficiency-lints` also enables this error.
```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)
```
## pytorch-efficiency-lints
Default severity: `ignore`
Umbrella error code for PyTorch GPU performance anti-patterns. Suppressing or enabling `pytorch-efficiency-lints` cascades to every sub-kind — useful when you want to surface (or silence) all of them without listing them individually.
All concrete diagnostics are emitted under one of the more specific sub-kinds:
- [`pytorch-efficiency-lint-cuda-call`](#pytorch-efficiency-lint-cuda-call)
- [`pytorch-efficiency-lint-item-call`](#pytorch-efficiency-lint-item-call)
- [`pytorch-efficiency-lint-print-tensor`](#pytorch-efficiency-lint-print-tensor)
- [`pytorch-efficiency-lint-redundant-to-call`](#pytorch-efficiency-lint-redundant-to-call)
## 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":
...
```
## regex
This error is raised when Pyrefly can statically detect an invalid regular expression pattern.
```python
import re
re.compile("(") # missing ), unterminated subpattern [regex]
```
## 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-argument-type
Default severity: `ignore`
A call argument whose type is an implicit `Any` (unknown) is passed without the type checker being able to verify it against the parameter, hiding potential bugs. This mirrors pyright's `reportUnknownArgumentType`.
```python
def untyped(x): # unannotated: returns an implicit `Any`
return x
def f(n: int) -> None: ...
f(untyped(1)) # the argument's type is unknown [unknown-argument-type]
```
## unknown-attribute-type
Default severity: `ignore`
An unannotated attribute is assigned a value whose type is unknown, so the attribute is inferred as `Any`. Add an explicit attribute annotation or fix the right-hand side so Pyrefly can infer a concrete type.
This is not a sub-kind of [implicit-any](#implicit-any): suppressing `implicit-any` does not suppress this error.
```python
def untyped(x): # unannotated: returns an implicit `Any`
return x
class C:
def __init__(self) -> None:
self.x = untyped(1) # downstream users don't know what type `self.x` is
self.y: int = untyped(1) # downstream users treat `self.y` as `int`
```
## unknown-column
Accessing a DataFrame column that does not exist in the inferred schema.
## 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
```
## unknown-variable-type
Default severity: `ignore`
A variable assigned a value of unknown type without an explicit annotation is inferred as `Any`, which hides type errors wherever it is used. Adding an annotation gives the variable a concrete type.
```python
def untyped(x): # unannotated: returns an implicit `Any`
return x
y = untyped(1) # downstream users don't know what type `y` is
y2: int = untyped(1) # downstream users treat `y2` as `int`
```
## 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`
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-dynamic-base
Default severity: `ignore`
This error is raised for dynamic class definitions created with `type()` when
the base classes are not statically known class literals.
```python
class Base: ...
def factory(base: type[Base]) -> type:
return type("Dynamic", (base,), {})
# Base class `type[Base]` in `type()` call is not a statically known class [unsupported-dynamic-base]
```
## unsupported-operation
This error arises when attempting to perform an operation between values of two incompatible types.
```python
x = 1 + "oops" # `+` is not supported between `int` and `str` [unsupported-operation]
```
## untyped-class-decorator
Default severity: `ignore`
A class decorator whose own type is `Any` may modify the class in unexpected ways.
```python
from typing import Any
my_decorator: Any = lambda cls: cls
@my_decorator # `my_decorator` is `Any`, so the type of `A` is lost [untyped-class-decorator]
class A: ...
```
## untyped-function-decorator
Default severity: `ignore`
A function or method decorator whose own type is `Any` obscures the decorated function's type.
```python
from typing import Any
my_decorator: Any = lambda f: f
@my_decorator # `my_decorator` is `Any`, so the type of `g` is lost [untyped-function-decorator]
def g() -> int:
return 1
```
## 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-call-result
Default severity: `ignore`
This rule is disabled by default and must be explicitly enabled. Once enabled, it reports when the
result of a call expression is discarded and the result type is informative (not `None`, `Any`, or
`Never`). This is often a sign of a mistake, such as forgetting to use the return value.
```python
def combine(a: list[int], b: list[int]) -> list[int]:
return a + b
items = [1, 2, 3]
combine(items, [4, 5]) # warning: result is discarded
print(items) # ok: print returns None
x = combine(items, [4, 5]) # ok: result is used
```
## 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.
## unused-type-ignore
Default severity: `ignore`
This error is raised when a `# type: ignore` comment is not used to suppress any error, and can be safely removed. This rule is distinct from `unused-ignore` so that projects using multiple type checkers can leave `# type: ignore` comments for other tools (e.g. mypy) without pyrefly flagging them. Enable this rule if your project uses pyrefly exclusively.
## useless-overload-body
Default severity: `warn`
This warning is raised when an `@overload` function contains executable body logic.
Overload bodies are never executed at runtime, so only placeholder bodies like `pass`, `...`,
a docstring-only body, `raise NotImplementedError(...)`, or `return NotImplemented` are useful.
```python
from typing import overload
@overload
def parse(x: int) -> int:
return x + 1 # warning: executable logic in an overload body
```
## 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 ""
`}
/>
To ignore one error type across a file, add the error code to the file-level comment:
```python
# pyrefly: ignore-errors[bad-assignment]
```
A file-level `ignore-errors` / `ignore-errors[code]` directive is only honored when it
appears at the beginning of the file, before any code (comments, blank lines, and the
module docstring may precede it). Placed after the first line of code it is silently
inert — it suppresses nothing — and Pyrefly reports a
[`misplaced-ignore`](../error-kinds#misplaced-ignore) warning pointing at it. To suppress
errors past the top of a file, use a line-level `# pyrefly: ignore[code]` instead.
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).
A baseline is the recommended starting point when migrating a large codebase from [mypy](./migrate/mypy/index.mdx) or [Pyright](./migrate/pyright/index.mdx).
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.
`--baseline-error-level=` and the project-level
`baseline-error-level` setting control how matching errors are reported. The
default is `ignore`, which omits them from CLI output; the other levels emit them
at a reduced or unchanged severity:
```toml
baseline = "baseline.json"
baseline-error-level = "warn"
```
Emitted matches follow the normal `--min-severity` filtering and exit-status rules.
Pyrefly records baseline provenance differently depending on the output format:
- `full-text` and `min-text`: matched findings include `[baselined]` after the error-kind marker.
- `github`: matched findings include `[baselined]` in the annotation title.
- `json`: when a baseline is configured, each result has `baselined` set to `true` for a match and `false` otherwise.
- `sarif`: when a baseline is configured, each result has `baselineState` set to `unchanged` for a match and `new` otherwise.
- `omit-errors`: the summary includes the number of emitted baselined diagnostics.
If a baseline file is configured but cannot be read or parsed, the run fails with an error rather than proceeding as if no baseline were set. The exception is `--update-baseline`, which regenerates the file from scratch and so tolerates a missing or unparseable baseline.
As you fix errors, the entries that suppressed them become stale. Two flags help keep the baseline current:
```
# Rewrite the baseline to drop stale entries, without recording any new errors.
pyrefly check --prune-baseline
# Exit with a non-zero status if the baseline contains stale entries.
pyrefly check --error-stale-baseline
```
Both actions use the scope of the current check. An unmatched entry is stale when its file was checked, or when the file is confirmed not to exist. Entries for existing files outside a narrowed check are retained, as are entries whose file status cannot be determined because of a filesystem error.
Pruning keeps entries for diagnostics that still occur, even when `--min-severity` hides them. This is intentionally conservative. `--update-baseline` instead regenerates the baseline from diagnostics at or above the severity threshold.
`--error-stale-baseline` is intended for CI, where an out-of-date baseline should fail the build the same way a new error does. `--prune-baseline`, `--error-stale-baseline`, and `--update-baseline` are mutually exclusive, and each requires a baseline file from `--baseline` or the configuration file.
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.
By default, `--remove-unused` removes Pyrefly and Pyre ignores and preserves `# type: ignore` comments because they may be shared with other type checkers. `--remove-unused=pyrefly` is equivalent to the bare flag. Use `pyrefly suppress --remove-unused=type` to remove only unused `# type: ignore` comments, or `pyrefly suppress --remove-unused=all` to remove all three kinds.
:::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[=KIND]` is equivalent to `pyrefly check --remove-unused-ignores[=KIND]`.
:::
---
---
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.
## Already using another type checker?
Pyrefly reads your existing mypy or Pyright configuration, so switching starts with a single command.
- [Compare Python type checkers](compare.mdx) - what Pyrefly gives you over mypy and Pyright
- [Migrate from mypy](migrate/mypy/index.mdx) - convert a mypy config and roll Pyrefly out incrementally
- [Migrate from Pyright](migrate/pyright/index.mdx) - convert a `pyrightconfig.json` and review the diagnostic mapping
---
---
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 Pyrefly configuration, it automatically looks for a nearby mypy or Pyright configuration to migrate in memory. If it does not find one, or migration fails, Pyrefly uses the **basic preset**. 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. When the fallback is active, the status bar shows `Pyrefly (Basic)` so you know which preset is active.
If Pyrefly finds a `mypy.ini`, `pyrightconfig.json`, or a `[tool.mypy]` / `[tool.pyright]` section in `pyproject.toml`, the status bar instead shows `Pyrefly (Legacy)` (mypy migration) or `Pyrefly (Default)` (Pyright migration). Nothing is written to disk during the migration; running `pyrefly init` later will commit the same migration permanently.
For what that migration does and does not preserve, see [migrating from mypy](../migrating-from-mypy) and [migrating from Pyright](../migrating-from-pyright).
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).
----
## Browser / WebAssembly
The [sandbox](https://pyrefly.org/sandbox/) runs Pyrefly in the browser via
WebAssembly. That path is for trying Pyrefly online and for website
development, not a replacement for the CLI or IDE extension. See
[WebAssembly](./wasm.mdx).
---
---
title: Migrating to Pyrefly
slug: /migrating-to-pyrefly
description:
Start here to move a Python project to Pyrefly from mypy or Pyright, with
automatic config conversion, parallel CI, and a suppression or baseline
rollout.
keywords:
- migrate to pyrefly
- python type checker migration
- mypy to pyrefly
- 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.
*/}
Pyrefly is designed to be adopted incrementally. You can install it beside your
current checker, keep both in CI, and drop the old one once the team has
accepted the differences.
Still deciding? [Mypy vs Pyright vs Pyrefly](../compare.mdx) covers the
comparison.
## Pick your starting point
- **[Migrate from mypy](mypy/index.mdx)** for a project configured with
`mypy.ini`, `setup.cfg`, or `[tool.mypy]`.
- **[Migrate from Pyright](pyright/index.mdx)** for a project configured with
`pyrightconfig.json` or `[tool.pyright]`, including Pylance users.
Both guides follow the same shape: install Pyrefly and convert the config, check
the mapping is faithful, then handle the remaining errors and drop your old
checker.
## The short version
1. Install Pyrefly alongside your current checker:
```sh
# uv
uv add --dev pyrefly
# pip
python -m pip install pyrefly
```
2. Convert your existing mypy or Pyright config into a native Pyrefly one:
```sh
pyrefly init
```
Read the generated config before committing it. Not every setting has an
exact Pyrefly equivalent, and unrecognized ones are skipped without a
warning. The guides above cover what to check.
3. Type check with the config you just generated:
```sh
pyrefly check
```
Your existing config is left in place, so both checkers can keep working
while you review the result.
4. Decide how to handle any new errors introduced by differing checker behavior.
Pyrefly offers two ways to clear the signal without fixing everything up
front:
- [Suppression comments](../error-suppressions.mdx#error-suppression-comments)
silence errors inline with `# pyrefly: ignore`, and `pyrefly suppress`
adds them across the project in bulk.
- A [baseline](../error-suppressions.mdx#baseline-files-experimental)
records the current errors in a file, so CI reports only newly introduced
ones and your source stays untouched. This is useful if you are migrating
a very large codebase.
5. When you're ready, remove your old type checker and any unused ignore
comments.
## Reference material
Each guide links to the detail it needs, and the full mappings are also
available directly:
| Coming from | Configuration | Diagnostics |
| ----------- | -------------------------------------------------------- | -------------------------------------------------------- |
| mypy | [Mypy config reference](mypy/config-reference.mdx) | [Mypy error codes](mypy/error-codes.mdx) |
| Pyright | [Pyright config reference](pyright/config-reference.mdx) | [Pyright diagnostics](pyright/diagnostics-reference.mdx) |
If config migration goes wrong for your project, please
[let us know](https://github.com/facebook/pyrefly/issues).
---
---
title: Mypy Config to Pyrefly
slug: /migrate/mypy/config
description:
A complete migration reference for translating mypy.ini and [tool.mypy]
options into Pyrefly configuration.
keywords:
- mypy config
- mypy pyproject toml
- mypy ini
- convert mypy config 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.
*/}
This page separates what `pyrefly init` converts today from the settings that
only have a manual or approximate Pyrefly equivalent.
:::warning
A conceptual equivalent is not necessarily implemented by the converter. Read
the generated config before trusting a row.
:::
## Quick conversion
```sh
pyrefly init
```
A typical generated config starts like this:
```toml
# pyrefly.toml
preset = "legacy"
project-includes = ["src"]
project-excludes = ["**/generated/**"]
search-path = ["src"]
python-version = "3.13"
[errors]
missing-import = "error"
```
## Mapping status
- **Automatic:** `pyrefly init` reads the mypy option and writes one or more
Pyrefly settings.
- **Automatic; review:** converted by `pyrefly init`, but the semantics differ
or the conversion is lossy so you should review.
- **Manual:** a related Pyrefly setting exists, but it is not captured by
`pyrefly init` so you should manually add it to your Pyrefly config.
- **Not mapped:** no current automatic or direct equivalent.
## Complete option matrix
| Mypy option | Closest Pyrefly setting or workflow | Status | Migration note |
| ---------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mypy_path` | `search-path` | Automatic | Both add import roots; verify ordering and config-relative paths. |
| `files` | `project-includes` | Automatic | Entries are converted to Pyrefly filesystem globs. |
| `modules` | `project-includes` | Automatic; review | Mypy does not recurse through `modules`; a path-oriented Pyrefly include may cover more files. |
| `packages` | `project-includes` | Automatic | Review namespace-package layouts. |
| `exclude` | `project-excludes` | Automatic; approximate | Mypy regexes are converted to globs. Complex regex features cannot be preserved exactly. |
| `exclude_gitignore` | `use-ignore-files` | Manual; not exact | Pyrefly reads `.gitignore`, `.ignore`, and `.git/info/exclude` by default, which is not the same rule set. |
| `namespace_packages` | No direct setting | Not mapped | Validate import resolution on namespace packages. |
| `explicit_package_bases` | `search-path` and project layout | Manual | Represent package roots explicitly rather than translating the flag. |
| `ignore_missing_imports` | `ignore-missing-imports` | Automatic | Per-module mypy overrides become module globs. Imports are replaced with `Any` only when they cannot be found. |
| `follow_untyped_imports` | `replace-untyped-imports-with-any` | Automatic | Mypy's default of `false` becomes `"*"`. Per-module overrides become positive or negative module globs. |
| `follow_imports` | `replace-imports-with-any` for `skip`; the converter emits `ignore-missing-imports` | Automatic but lossy for `skip` | Mypy `skip` discards an import even when it resolves. The converter groups it with `ignore_missing_imports`, which is narrower, so change the generated entry by hand when exact `skip` behavior matters. `normal`, `silent`, and `error` have no equivalent. |
| `follow_imports_for_stubs` | No direct setting | Not mapped | |
| `python_executable` | `python-interpreter-path` | Automatic | Prefer environment autoconfiguration where possible. |
| `no_site_packages` | `skip-interpreter-query` plus explicit `site-package-path` | Manual; not exact | Skipping the interpreter query also affects the inferred Python version and platform. |
| `no_silence_site_packages` | No direct setting | Not mapped | Pyrefly treats site packages as dependencies rather than project files. |
| `python_version` | `python-version` | Automatic | |
| `platform` | `python-platform` | Manual | The settings are equivalent, but the mypy converter does not wire this one through. Set `python-platform` explicitly. |
| `always_true` | No direct setting | Not mapped | |
| `always_false` | No direct setting | Not mapped | |
| `disallow_any_unimported` | `untyped-import`, `missing-import`, and the implicit-`Any` kinds | Manual; no exact 1:1 | |
| `disallow_any_expr` | No direct setting | Not mapped | Pyrefly has targeted explicit and implicit `Any` diagnostics, not a general expression-level ban. |
| `disallow_any_decorated` | No direct setting | Not mapped | |
| `disallow_any_explicit` | `[errors] explicit-any = "error"` | Automatic | |
| `disallow_any_generics` | `[errors] implicit-any = "error"` | Automatic; broader | For closer semantics, enable only `implicit-any-type-argument`. |
| `disallow_subclassing_any` | `unsupported-dynamic-base` | Manual; not exact | |
| `disallow_untyped_calls` | No direct setting | Not mapped | |
| `untyped_calls_exclude` | No direct setting | Not mapped | |
| `disallow_untyped_defs` | `implicit-any-parameter` + `unannotated-return` | Automatic | |
| `disallow_incomplete_defs` | `implicit-any-parameter` + `unannotated-return` | Automatic; broader | The Pyrefly pair does not distinguish partially annotated from fully unannotated definitions the way mypy does. |
| `check_untyped_defs` | `check-unannotated-defs = true` and `infer-return-types = "never"` | Automatic; global only | Both the INI and `[tool.mypy]` paths read this field, falling back to `strict`. Per-module sections are ignored, so a config that only enables it per module needs a manual review. |
| `disallow_untyped_decorators` | `untyped-function-decorator`, `untyped-class-decorator` | Manual | |
| `implicit_optional` | No direct setting | Not mapped | Pyrefly follows modern explicit-`Optional` behavior. |
| `strict_optional` | No direct setting | Not mapped | Pyrefly has no equivalent of mypy's legacy non-strict `Optional` mode. |
| `warn_redundant_casts` | `[errors] redundant-cast = "warn"` | Automatic | |
| `warn_unused_ignores` | `unused-type-ignore` and/or `unused-ignore` | Manual | Choose based on which ignore syntax you are enforcing. |
| `warn_no_return` | `bad-return` behavior | No config translation needed | Pyrefly does not expose this as a standalone toggle. |
| `warn_return_any` | `[errors] no-any-return = "error"` | Automatic | |
| `warn_unreachable` | `[errors] unreachable = "warn"` | Manual | |
| `deprecated_calls_exclude` | Sub-config or source suppression | Manual; no exact 1:1 | |
| `ignore_errors` | Baseline, `pyrefly suppress`, or sub-configs | Manual; no exact 1:1 | Prefer a baseline for incremental rollout rather than excluding code outright. |
| `allow_untyped_globals` | No direct setting | Not mapped | |
| `allow_redefinition` | `[errors] redefinition = "ignore"` | Manual | The converter reads the key `allow_redefinitions`, not mypy's documented `allow_redefinition`, so do not assume this is preserved automatically. |
| `allow_redefinition_new` | `[errors] redefinition = "ignore"` | Manual; approximate | Pyrefly has no equivalent of mypy's old/new redefinition split. |
| `allow_redefinition_old` | `[errors] redefinition = "ignore"` | Manual; approximate | Pyrefly has no equivalent of mypy's old/new redefinition split. |
| `local_partial_types` | No direct setting | Not mapped | |
| `disable_error_code` | `[errors]` or `[sub-config.errors]` | Automatic for mapped codes | Unrecognized mypy codes are dropped by the converter; audit the generated config. |
| `enable_error_code` | `[errors]` or `[sub-config.errors]` | Automatic for mapped codes | Unrecognized mypy codes are dropped by the converter; audit the generated config. |
| `extra_checks` | Specific error kinds, or `preset = "strict"` | Manual | There is no matching bundle. |
| `implicit_reexport` | `[errors] implicit-reexport = ...` | Manual | |
| `strict_equality` | `incompatible-comparison`, `unnecessary-comparison` | Manual | |
| `strict_equality_for_none` | No separate setting | Not mapped | |
| `strict_bytes` | No direct setting | Not mapped | Validate `bytes` and `bytearray` behavior on representative code. |
| `strict` | `preset = "legacy"` plus selected error overrides | Automatic; incomplete | The two strict bundles differ and evolve independently. The converter maps the error policies listed in [Mypy error codes](error-codes.mdx), but does not fully expand mypy strict. In particular, verify `check-unannotated-defs = true`, because mypy strict includes `check_untyped_defs`. |
| `show_error_context` | `output-format` and CLI output | No 1:1 mapping | |
| `show_column_numbers` | Pyrefly output always includes locations | No 1:1 mapping | |
| `show_error_code_links` | No direct setting | Not mapped | |
| `hide_error_codes` | No direct setting | Not mapped | |
| `pretty` | `output-format = "full-text"` | Manual; approximate | |
| `color_output` | Terminal behavior | No 1:1 mapping | |
| `error_summary` | No direct setting | Not mapped | |
| `show_absolute_path` | No direct setting | Not mapped | |
| `incremental` | No user-facing equivalent | Not mapped | Pyrefly manages its own incremental analysis. |
| `cache_dir` | No user-facing equivalent | Not mapped | |
| `sqlite_cache` | No user-facing equivalent | Not mapped | |
| `cache_fine_grained` | No user-facing equivalent | Not mapped | |
| `skip_version_check` | No user-facing equivalent | Not mapped | |
| `skip_cache_mtime_checks` | No user-facing equivalent | Not mapped | |
| `num_workers` | No direct setting | Not mapped | Benchmark Pyrefly's default execution on the target repository. |
| `plugins` | No plugin-loader setting | Not mapped | Pyrefly does not execute arbitrary mypy plugins, but built-in Pydantic v2+, Django, and attrs support replaces several common plugin use cases. Review custom plugins separately. |
| `pdb` | No direct setting | Not mapped | |
| `show_traceback` | No direct setting | Not mapped | |
| `raise_exceptions` | No direct setting | Not mapped | |
| `custom_typing_module` | No direct setting | Not mapped | |
| `custom_typeshed_dir` | `typeshed-path` | Manual | |
| `warn_incomplete_stub` | No direct setting | Not mapped | |
| `native_parser` | No direct setting | Not mapped | |
| `any_exprs_report` | `pyrefly coverage` | Manual; not equivalent | Coverage reports measure annotation and type coverage, not mypy's `Any`-expression report. |
| `cobertura_xml_report` | External coverage tooling | Manual | |
| `html_report` / `xslt_html_report` | External coverage tooling | Manual | |
| `linecount_report` | `pyrefly coverage` | Manual | |
| `linecoverage_report` | `pyrefly coverage` | Manual | |
| `lineprecision_report` | `pyrefly coverage` | Manual | |
| `txt_report` / `xslt_txt_report` | External coverage tooling | Manual | |
| `xml_report` | External coverage tooling | Manual | |
| `junit_xml` | `output-format = "junit-xml"` | Manual | |
| `junit_format` | No direct setting | Not mapped | |
| `scripts_are_modules` | No direct setting | Not mapped | |
| `warn_unused_configs` | No direct setting | Not mapped | Pyrefly rejects unknown options in its own config files. |
| `verbosity` | CLI logging flags | No 1:1 mapping | |
## Plugin configuration
`pyrefly init` does not copy mypy's `plugins` option. That does **not** mean a
plugin-backed project loses framework-aware checking:
| Mypy integration | Pyrefly equivalent |
| ------------------------------ | -------------------------------------------------------------------------- |
| `pydantic.mypy` | Built-in [Pydantic v2+ support](../../pydantic.mdx), enabled automatically |
| `mypy_django_plugin.main` | Built-in [Django support](../../django.mdx) with `django-stubs` installed |
| Mypy's built-in attrs handling | Built-in [attrs support](../../attrs.mdx) for attrs 23.2+ |
| A custom plugin module | No equivalent; replace manually or keep mypy |
Keep the mypy `plugins` entry while mypy still runs in parallel CI. Pyrefly
simply does not consume it. See [Mypy plugins](plugins.mdx) for the migration
checklist and the framework-specific caveats.
## Per-module overrides
Mypy matches **module names**. Pyrefly
[sub-configs](../../configuration.mdx#sub-configs) match **file paths** and
support only:
- `errors`;
- `replace-imports-with-any`;
- `check-unannotated-defs`;
- `infer-return-types`;
- `ignore-errors-in-generated-code`.
Example:
```toml
[[sub-config]]
matches = "src/legacy/**"
check-unannotated-defs = false
infer-return-types = "never"
[sub-config.errors]
implicit-any = "ignore"
```
Mypy per-module import suppression is migrated separately, into the
`ignore-missing-imports` module-glob list rather than into a sub-config.
## High-risk review items
1. **Custom plugins:** Pyrefly does not execute arbitrary mypy plugin modules.
Pydantic, Django, and attrs are [native-support cases](plugins.mdx) to
validate, not wholesale gaps.
2. **Regex excludes:** complex regexes cannot be preserved exactly as globs.
3. **`modules`:** mypy and Pyrefly differ in recursion and project discovery.
4. **Strict mode:** mypy strict is not Pyrefly strict.
5. **Platform:** set `python-platform` explicitly and verify it.
6. **Per-module `check_untyped_defs`:** review by hand; only the global setting
is migrated.
7. **`allow_redefinition`:** review by hand; the converter reads a different key
spelling.
8. **Error codes:** only recognized codes become Pyrefly error overrides.
## Upstream references
- [Mypy configuration file documentation](https://mypy.readthedocs.io/en/stable/config_file.html)
- [Pyrefly configuration](../../configuration.mdx)
## Next steps
- [Migrate from mypy](index.mdx)
- [Mypy error codes](error-codes.mdx)
- [Mypy strict mode](strict-mode.mdx)
- [Pyrefly configuration](../../configuration.mdx)
:::note
Last reviewed against upstream mypy documentation and Pyrefly's migration source
on 2026-08-08.
:::
---
---
title: Mypy Error Codes to Pyrefly
slug: /migrate/mypy/error-codes
description:
Map mypy error codes to Pyrefly error kinds, with exact automatic migration
status and one-to-many compatibility notes.
keywords:
- mypy error codes
- mypy disable error code
- mypy error code mapping
- pyrefly error kinds
---
{/*
* 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.
*/}
Mypy configs often set severity policy by error code, in `disable_error_code`,
`enable_error_code`, and per-module overrides. Pyrefly expresses the same policy
with [error kinds](../../error-kinds.mdx), which use different names, so
`pyrefly init` has to translate each code as it converts the config.
Some mypy error codes are not able to be translated by `pyrefly init` or when
the two tools group the same scenario differently a translated code is not
always an exact equivalent. Review the table below to compare mypy and Pyrefly
error codes.
## How to read the table
- **Automatic:** `pyrefly init` translates the code.
- **Manual:** a useful Pyrefly kind exists, but `pyrefly init` does not
translate it, so you should update your Pyrefly config manually.
- **Split / broad / related:** a similar error kind exists in Pyrefly but do not
assume equal coverage in both directions.
- **Not mapped:** no current one-to-one translation.
:::warning
This table describes **configuration migration**. Enabling the mapped Pyrefly
kind does not make every diagnostic identical to mypy's.
:::
## Complete mapping
| Mypy code | Closest Pyrefly kind(s) | Config migration | Fidelity | Notes |
| --------------------------------- | --------------------------------------------------------------------------- | ---------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `attr-defined` | `missing-attribute` | Automatic | Direct-ish | Pyrefly reports missing module attributes as `missing-module-attribute`. |
| `name-defined` | `unknown-name` | Automatic | Direct | |
| `call-arg` | `bad-argument-count` | Automatic | Partial | Pyrefly also has more specific call-shape kinds such as `missing-argument`, `unexpected-keyword`, and `unexpected-positional-argument`. |
| `arg-type` | `bad-argument-type` | Automatic | Direct-ish | |
| `call-overload` | `no-matching-overload` | Automatic | Direct-ish | Pyrefly may also emit `incompatible-overload-residual` in higher-order generic calls. |
| `valid-type` | `invalid-annotation` | Automatic | Broad | Some invalid type forms are split into `invalid-literal`, `invalid-type-alias`, `invalid-type-var`, or `not-a-type`. |
| `nonetype-type` | `invalid-annotation` | Manual | Related | No dedicated one-to-one kind. |
| `var-annotated` | `implicit-any-empty-container`, `implicit-any`, or a context-specific error | Manual | Related | Pyrefly splits implicit `Any` by source rather than using one "annotation required" code. |
| `override` | `bad-override` | Automatic | Direct-ish | Pyrefly has sub-kinds for mutable attributes and parameter names. |
| `return` | `bad-return` | Automatic | Broad | Mypy uses this for missing-return paths; Pyrefly groups incompatible and missing returns under `bad-return`. |
| `return-value` | `bad-return` | Automatic | Direct-ish | |
| `assignment` | `bad-assignment` | Automatic | Direct-ish | |
| `method-assign` | `bad-assignment` or `read-only` | Manual | Related | Depends on whether the failure is type incompatibility or attempted mutation. |
| `type-arg` | `implicit-any-type-argument` | Automatic | Direct-ish | |
| `type-var` | `bad-specialization` | Automatic | Partial | Pyrefly uses `invalid-type-var` for many TypeVar definition and usage errors. |
| `union-attr` | `missing-attribute` | Automatic | Direct-ish | |
| `index` | `bad-index`, `unsupported-operation` | Automatic | Split | One mypy code maps to two Pyrefly kinds. |
| `operator` | `unsupported-operation` | Automatic | Direct-ish | |
| `list-item` | Usually `bad-assignment` or `bad-argument-type` | Manual | No stable 1:1 | The Pyrefly kind depends on the surrounding expected type. |
| `dict-item` | `bad-typed-dict` | Automatic | Approximate | This is what the converter emits, but ordinary dict-literal item errors are not semantically identical to `bad-typed-dict`; review the generated override. |
| `typeddict-item` | `bad-typed-dict` or `bad-typed-dict-key` | Manual | Related | |
| `typeddict-unknown-key` | `bad-typed-dict-key` | Automatic | Direct | |
| `has-type` | No direct equivalent | Not mapped | None | Pyrefly reports a more specific inference or definition error instead. |
| `import` | `missing-import` | Automatic | Broad | Mypy's umbrella import code also covers missing stubs; Pyrefly separates `missing-import` and `untyped-import`. |
| `import-not-found` | `missing-import` | Automatic | Direct | |
| `import-untyped` | `untyped-import` | Automatic | Direct-ish | |
| `no-redef` | `redefinition` | Manual | Direct-ish | The converter does not translate `no-redef`. |
| `func-returns-value` | Usually `bad-assignment` or `bad-return` | Manual | No stable 1:1 | |
| `abstract` | `bad-instantiation` | Automatic | Direct-ish | |
| `type-abstract` | `bad-instantiation` | Manual | Related | Pyrefly has no separate "abstract class object passed where a concrete type is expected" kind. |
| `valid-newtype` | `invalid-argument` | Manual | Related | |
| `str-format` | No direct equivalent | Not mapped | None | Use runtime tests or a linter for format-string checks. |
| `str-bytes-safe` | No direct equivalent | Not mapped | None | |
| `exit-return` | `bad-return` | Manual | Related | |
| `literal-required` | `invalid-literal` | Manual | Direct-ish | |
| `unused-coroutine` | `unused-coroutine` | Automatic | Direct | |
| `empty-body` | `empty-body` | Manual | Direct | |
| `safe-super` | `abstract-method-call` | Manual | Related | |
| `top-level-await` | `not-async` | Automatic | Direct-ish | |
| `await-not-async` | `not-async` | Automatic | Direct-ish | |
| `no-untyped-def` | `implicit-any-parameter`, `unannotated-return` | Automatic | Split | The `implicit-any` umbrella enables every implicit-`Any` sub-kind, which is broader than mypy's code. |
| `no-untyped-call` | No direct equivalent | Not mapped | None | Pyrefly reports the downstream unknown/`Any` consequences rather than the call itself. |
| `redundant-cast` | `redundant-cast` | Automatic | Direct | |
| `assert-type` | `assert-type` | Automatic | Direct | |
| `comparison-overlap` | `incompatible-comparison`, `unnecessary-comparison` | Manual | Split/related | |
| `no-any-unimported` | `untyped-import`, `missing-import`, and the implicit-`Any` kinds | Manual | No exact 1:1 | The tools model import-derived `Any` differently. |
| `no-any-return` | `no-any-return` | Automatic | Direct-ish | Pyrefly has explicit and implicit sub-kinds under this umbrella. |
| `unreachable` | `unreachable` | Manual | Direct-ish | Pyrefly also has `unreachable-match-case`. |
| `annotation-unchecked` | No direct equivalent | Not mapped | None | Pyrefly's checking of unannotated functions is controlled by `check-unannotated-defs`. |
| `typeddict-readonly-mutated` | `read-only` | Automatic | Direct-ish | |
| `possibly-undefined` | `unbound-name` | Automatic | Direct-ish | |
| `redundant-expr` | `redundant-condition` | Automatic | Related | |
| `truthy-bool` | `redundant-condition` | Automatic | Related | |
| `truthy-function` | `redundant-condition` | Automatic | Related | |
| `truthy-iterable` | `redundant-condition` | Automatic | Related | |
| `str-unpack` | `string-as-iterable` | Manual | Related | |
| `name-match` | `name-mismatch` | Automatic | Direct | |
| `no-overload-impl` | `invalid-overload` | Automatic | Direct | |
| `ignore-without-code` | No direct equivalent | Not mapped | None | Pyrefly can detect unused or misplaced ignores, but not this exact policy. |
| `unused-awaitable` | `unused-coroutine` | Automatic | Related | Mypy's awaitable rule is broader than coroutines. |
| `redundant-self` | No direct equivalent | Not mapped | None | |
| `used-before-def` | `unbound-name` | Automatic | Direct-ish | |
| `unused-ignore` | `unused-type-ignore` or `unused-ignore` | Manual | Direct-ish | Choose based on the ignore syntax you are enforcing. |
| `explicit-override` | `missing-override-decorator` | Manual | Direct | |
| `unimported-reveal` | `unimported-directive` | Manual | Direct-ish | |
| `mutable-override` | `bad-override-mutable-attribute` | Automatic | Direct | |
| `exhaustive-match` | `non-exhaustive-match` | Manual | Direct | |
| `metaclass` | `invalid-inheritance` | Automatic | Related | |
| `maybe-unrecognized-str-typeform` | `invalid-annotation` or `not-a-type` | Manual | Related | |
| `syntax` | `parse-error` | Automatic | Direct-ish | Pyrefly also has `invalid-syntax` for checker-level syntax restrictions. |
| `misc` | No direct umbrella | Not mapped | None | Map the underlying scenario, not mypy's catch-all code. |
| `overload-cannot-match` | `inconsistent-overload` | Manual | Related | No exact one-to-one rule. |
| `overload-overlap` | `inconsistent-overload` | Manual | Related | No exact one-to-one rule. |
| `prop-decorator` | `invalid-decorator` | Manual | Related | |
| `untyped-decorator` | `untyped-function-decorator` | Manual | Direct-ish | Class decorators use `untyped-class-decorator`. |
| `narrowed-type-not-subtype` | No direct equivalent | Not mapped | None | A more specific annotation or argument error may be emitted instead. |
| `explicit-any` | `explicit-any` | Automatic | Direct | |
| `deprecated` | `deprecated` | Automatic | Direct | |
## Configuration example
Mypy:
```toml
[tool.mypy]
enable_error_code = ["explicit-override", "possibly-undefined"]
disable_error_code = ["import-untyped"]
```
A reviewed Pyrefly equivalent is:
```toml
[errors]
missing-override-decorator = "error"
unbound-name = "error"
untyped-import = "ignore"
```
`explicit-override` is not in the converter's code map even though Pyrefly has a
close equivalent, so `missing-override-decorator` has to be added by hand.
## Strictness flags that become error kinds
`pyrefly init` also converts a handful of mypy flags directly into error
severities:
| Mypy flag | Generated Pyrefly policy | Caveat |
| ---------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ |
| `warn_return_any` or `strict` | `no-any-return = "error"` | Pyrefly has explicit and implicit sub-kinds |
| `warn_redundant_casts` or `strict` | `redundant-cast = "warn"` | Generated at warning severity, not error |
| `disallow_untyped_defs`, `disallow_incomplete_defs`, or `strict` | `implicit-any-parameter` and `unannotated-return` as errors | The two tools divide partially annotated definitions differently |
| `disallow_any_generics` or `strict` | `implicit-any = "error"` | This umbrella is broader than missing generic arguments alone |
| `disallow_any_explicit` | `explicit-any = "error"` | Not enabled by mypy `strict` |
| `allow_redefinitions` | `redefinition = "ignore"` | The converter reads this key, not mypy's documented `allow_redefinition` |
## Related guides and references
- [Migrate from mypy](index.mdx)
- [Mypy config reference](config-reference.mdx)
- [Mypy strict mode](strict-mode.mdx)
- [Pyrefly error kinds](../../error-kinds.mdx)
- [Mypy default error codes](https://mypy.readthedocs.io/en/stable/error_code_list.html)
- [Mypy optional error codes](https://mypy.readthedocs.io/en/stable/error_code_list2.html)
:::note
Last reviewed against upstream mypy error-code documentation and Pyrefly's
migration source on 2026-08-08.
:::
---
---
title: Mypy ignore_missing_imports in Pyrefly
slug: /migrate/mypy/ignore-missing-imports
description:
Understand mypy ignore_missing_imports, its Pyrefly equivalent, and when to
use ignore-missing-imports instead of replacing all imports with Any.
keywords:
- mypy ignore_missing_imports
- ignore missing imports mypy
- pyrefly missing import
- mypy import error
---
{/*
* 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.
*/}
Mypy's `ignore_missing_imports` suppresses errors when an imported module cannot
be resolved. In a per-module override the pattern matches the **imported
module**, not the file that contains the import. Pyrefly's equivalent works the
same way.
## Mypy configuration
```toml
[tool.mypy]
[[tool.mypy.overrides]]
module = ["optional_vendor.*"]
ignore_missing_imports = true
```
## Pyrefly equivalent
```toml
ignore-missing-imports = ["optional_vendor.*"]
```
`pyrefly init` converts both per-module and global mypy import suppression into
this list. A global `ignore_missing_imports = true` becomes the module glob
`"*"`.
## Do not confuse the two Pyrefly settings
| Setting | Behavior |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `ignore-missing-imports` | Substitutes `Any` only when the matched module cannot be found. If it is found, its real type information is used. |
| `replace-imports-with-any` | Always substitutes `Any` for the matched module, even when source or stubs are available. |
| `replace-untyped-imports-with-any` | Substitutes `Any` when a matched installed package has neither stubs nor a `py.typed` marker. |
| `[errors] missing-import = "ignore"` | Suppresses missing-import diagnostics everywhere, rather than for selected modules. |
Prefer `ignore-missing-imports` for optional dependencies.
`replace-imports-with-any` is the closer match for mypy's
`follow_imports = "skip"`, and it is a much stronger escape hatch.
Mypy's `follow_untyped_imports` has the opposite polarity from
`replace-untyped-imports-with-any`. `pyrefly init` converts mypy's default of
`false` to `["*"]`. It converts per-module overrides to module globs, using
negated globs when a module enables `follow_untyped_imports` against that
global default.
:::warning
`pyrefly init` currently groups `follow_imports = "skip"` with
`ignore_missing_imports`, so a migrated `skip` rule lands in
`ignore-missing-imports` and keeps type information for modules that do resolve.
Inspect the generated list and move entries to `replace-imports-with-any` where
you need mypy's unconditional `skip` behavior.
:::
## Diagnose before suppressing
An unresolved import is often a misconfigured environment rather than a missing
dependency. Check that:
1. the dependency is installed in the interpreter Pyrefly queries;
2. `python-interpreter-path` selects the intended environment;
3. your source roots are listed in `search-path`;
4. custom stubs live in `site-package-path`;
5. the module name matches the package layout.
If the module resolves from the command line but not in the IDE, or the other
way round, the two are using different configs or different interpreters.
## Example migration
Mypy:
```ini
[mypy]
mypy_path = src
[mypy-vendor_sdk.*]
ignore_missing_imports = True
```
Pyrefly:
```toml
search-path = ["src"]
ignore-missing-imports = ["vendor_sdk.*"]
```
Pyrefly supports 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) for the exact
matching rules.
## Related guides
- [Migrate from mypy](index.mdx)
- [Mypy config reference](config-reference.mdx)
- [`reportMissingImports` for Pyright users](../pyright/missing-imports.mdx)
- [Import resolution](../../import-resolution.mdx)
---
---
title: Migrate from mypy
slug: /migrating-from-mypy
description:
'Move a Python project from mypy to Pyrefly: install alongside, convert the
config automatically, compare the two outputs, and adopt the backlog with
suppressions or a baseline.'
keywords:
- migrate from mypy
- mypy to pyrefly
- mypy alternative
- mypy config migration
- pyrefly init
---
{/*
* 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.
*/}
This guide covers the simplest migration path to transition from mypy to
Pyrefly. It should be suitable for most projects.
If your project uses mypy plugins or sets `strict = true`, follow this page
first and then read the [linked guide](#going-further) for that case.
## 1. Install Pyrefly and convert your config
```sh
# uv
uv add --dev pyrefly
# pip
python -m pip install pyrefly
```
```sh
pyrefly init
```
The `pyrefly init` command finds your `mypy.ini` or `[tool.mypy]` section and
writes the equivalent `pyrefly.toml` or `[tool.pyrefly]`, preserving the
settings it can: project inputs and import roots, Python version and interpreter
settings, import suppression, selected strictness flags, per-module error
overrides, and a subset of mypy error codes. Use `pyrefly init --help` for more
options.
Your mypy config is left in place. Pyrefly only reads its own config once one
exists, so both checkers keep working and you can back out at any point.
## 2. Check the config was mapped correctly
Read the generated config line by line before committing it. Settings and error
codes the converter does not recognize are dropped without a warning. The
[mypy config reference](config-reference.mdx) and
[mypy error-code reference](error-codes.mdx) show how each mypy option is
mapped, so you can check anything the generated file does not mention.
Alongside your converted settings, `pyrefly init` writes:
```toml
preset = "legacy"
```
The [`legacy` preset](../../configuration.mdx#preset-legacy) disables a few
checks that mypy does not have, so a migrating project is not immediately
flooded with errors for classes of issue mypy never flagged.
### Unannotated code
Mypy skips the bodies of completely unannotated functions. Pyrefly checks them
and can infer their return types, which gives better editor information but
produces differences during a migration. To keep mypy's treatment while you
migrate:
```toml
preset = "legacy"
check-unannotated-defs = false
infer-return-types = "never"
```
If mypy sets `check_untyped_defs = true`, including via `strict = true`, use
`check-unannotated-defs = true` instead.
:::warning
Both migration paths read `check_untyped_defs`, falling back to `strict`, but
only at the global level. A `check_untyped_defs` set under a per-module section
(`[mypy-some.module]`, or a `[[tool.mypy.overrides]]` entry) is dropped, so set
`check-unannotated-defs` by hand if your config only enables it per module. See
[mypy strict mode](strict-mode.mdx).
:::
Empty-container inference already matches mypy closely. Pyrefly infers the
element type from first use, controlled by `infer-with-first-use`, which
defaults to `true` and is normally left alone for a mypy migration.
## 3. Handle the new errors and drop mypy
With the config settled, run both checkers on the same commit and compare:
```sh
mypy src
pyrefly check
```
You may still find some differences in error output as Pyrefly implements the
type system independently of mypy and will not reproduce its diagnostics
exactly.
Classify each difference: a true bug, an inference difference, a framework
behavior gap, a config gap, or a custom-plugin dependency.
Once you know what the differences are, decide how to absorb the ones you are
not fixing up front. Start with suppressions: `pyrefly suppress` inserts
comments across the project for you, and `# pyrefly: ignore[bad-assignment]`
silences a single line.
```sh
pyrefly suppress
```
The comments live next to the code they apply to, so the remaining work stays
visible and can be cleared file by file.
If your codebase is large enough that suppression comments would mean churn
across thousands of lines, use a
[baseline](../../error-suppressions.mdx#baseline-files-experimental) instead:
```sh
pyrefly check --baseline="pyrefly-baseline.json" --update-baseline
pyrefly check --baseline="pyrefly-baseline.json"
```
Or commit it in the configuration:
```toml
baseline = "pyrefly-baseline.json"
```
A baseline records the current errors in a file rather than in your source. It
suppresses matching errors in CLI and CI runs so new errors can be blocked,
while still showing the suppressed diagnostics in the IDE. That lets the
migration land before the backlog is cleared.
Whichever you choose, Pyrefly respects `# type: ignore` by default; to also
honor mypy-specific ignores during the transition:
```toml
enabled-ignores = ["type", "mypy", "pyrefly"]
```
Keep that temporary and remove stale tool-specific comments once mypy leaves CI.
See the [error suppression docs](../../error-suppressions.mdx).
Once CI is green on Pyrefly alone and the team is happy with the editor
experience, drop mypy: remove it from your dependencies and CI, delete
`mypy.ini` or the `[tool.mypy]` section, and narrow `enabled-ignores` back to
`["type", "pyrefly"]`. Then clear the suppressions or the baseline as you go
with `pyrefly suppress --remove-unused`.
## Going further
| If your project | Read |
| ---------------------------------------------------------- | ----------------------------------------------------------- |
| Sets `plugins = [...]` | [Mypy plugins](plugins.mdx) |
| Sets `strict = true` | [Mypy strict mode](strict-mode.mdx) |
| Uses `ignore_missing_imports` or `follow_imports = "skip"` | [Mypy `ignore_missing_imports`](ignore-missing-imports.mdx) |
| Needs the full option-by-option mapping | [Mypy config reference](config-reference.mdx) |
| Needs the full error-code mapping | [Mypy error codes](error-codes.mdx) |
If a specific plugin, error code, or configuration option is what stops you from
adopting Pyrefly, please
[open an issue](https://github.com/facebook/pyrefly/issues).
---
---
title: Mypy Plugin Support in Pyrefly
slug: /migrate/mypy/plugins
description:
Migrate common mypy plugin workloads to Pyrefly's built-in Pydantic, Django,
and attrs support, and identify custom plugins that still need manual
review.
keywords:
- mypy plugins
- pyrefly mypy plugin support
- pydantic mypy plugin
- django mypy plugin
- attrs mypy plugin
- migrate mypy plugins
---
{/*
* 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 has first-party support for several of the most common mypy plugin
workloads, including Pydantic, Django, and attrs. The implementation model is
different: Pyrefly does not import Python modules written against `mypy.plugin`.
Those integrations are modeled directly in the checker and the language server
instead.
For those workloads, migrating means dropping mypy-only plugin configuration
from the Pyrefly side while keeping framework-aware type checking.
## At a glance
| Mypy integration | Pyrefly path | Migration status | What to verify |
| --------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `pydantic.mypy` | Built-in [Pydantic support](../../pydantic.mdx) | Native; no plugin or manual enablement | Pydantic v2+ models, generated signatures, strict/lax coercion, aliases, extra fields, project-specific validators |
| `mypy_django_plugin.main` from `django-stubs` | Built-in [Django support](../../django.mdx), using installed `django-stubs` | Native; no Pyrefly plugin config | Models, field types, `id` and `pk`, relationships, choices, managers, class-based views |
| Mypy's built-in attrs integration | Built-in [attrs support](../../attrs.mdx) | Native and automatic for attrs 23.2+ | Modern and classic decorators, fields, factories, converters, frozen classes, inheritance |
| A custom or proprietary mypy plugin | No plugin-loader equivalent | Manual review | Every hook, generated member, decorator transform, and custom semantic assumption |
:::note
"Pyrefly supports mypy plugin use cases" does not mean Pyrefly executes mypy
plugin code. It means the common plugin-heavy frameworks have native support
inside Pyrefly.
:::
## Pydantic: replace `pydantic.mypy` with native support
A mypy project typically has:
```toml
[tool.mypy]
plugins = ["pydantic.mypy"]
```
Pyrefly needs no equivalent entry. It recognizes Pydantic v2 constructs
including `BaseModel`, `Field`, `ConfigDict`, `pydantic_settings.BaseSettings`,
and Pydantic dataclasses, and it reads model behavior such as strict validation
and extra-field policy from the code rather than from plugin configuration.
Keep these differences in mind:
- Pydantic v1-only behavior is outside Pyrefly's documented support.
- The mypy plugin's options do not translate one-to-one, because Pyrefly derives
more policy from the model definition itself.
- Test aliases, validators, settings models, nested containers, and any
project-specific metaprogramming before retiring mypy.
See [Pydantic support](../../pydantic.mdx) for the current feature set and its
limitations.
## Django: replace the plugin, keep `django-stubs`
A mypy setup commonly contains:
```toml
[tool.mypy]
plugins = ["mypy_django_plugin.main"]
[tool.django-stubs]
django_settings_module = "myproject.settings"
```
For Pyrefly, install `django-stubs` but do not add any Pyrefly plugin entry.
Pyrefly models Django models and fields, auto-generated `id` and `pk`
attributes, relationships, choices, and model-aware class-based views.
Pyrefly's Django support is static: it follows stubs and models the code rather
than running Django or introspecting settings at type-check time. That removes
the plugin setup, but some relationship-manager and highly dynamic metaclass
cases can produce different types. Test representative models and queries rather
than assuming identical output.
See [Django support](../../django.mdx) for setup, supported behavior, and
documented differences from mypy.
## attrs: built in, with no configuration
Mypy ships attrs handling internally, so an attrs project may have no `plugins`
entry at all even though the checker is doing much more than stub lookup. This
is easy to overlook when inventorying what a migration has to replace.
Pyrefly also has built-in attrs support, covering the modern API (`@define`,
`@frozen`, `@mutable`, `attrs.field`) and the classic API (`@attr.s`,
`@attr.ib`, `@attr.dataclass`) across both the `attr` and `attrs` namespaces.
Support is automatic for attrs 23.2 and newer.
See [attrs support](../../attrs.mdx) for decorators, fields, converters,
factories, validators, inheritance, and helper functions.
## Custom plugins need a decision
A custom plugin may change:
- generated methods or attributes;
- decorator and metaclass semantics;
- function signatures or return types;
- dynamic ORM or schema behavior;
- type-analysis rules for a proprietary API.
Pyrefly does not run those hooks. For each custom plugin, pick one of four
paths:
1. model the behavior with standard annotations, `Protocol`, `TypedDict`,
overloads, or `dataclass_transform`;
2. ship or improve `.pyi` stubs;
3. request or contribute native Pyrefly support when the library is broadly
useful;
4. keep mypy for that project until the behavior has an acceptable replacement.
Do not suppress the resulting errors before understanding what the plugin was
providing.
## FAQ
### Does Pyrefly support mypy plugins?
Pyrefly supports the important **mypy plugin workloads** natively, including
Pydantic, Django, and attrs. It does not expose mypy's plugin API or execute
arbitrary mypy plugin modules.
### Should I delete `plugins` from `[tool.mypy]` immediately?
No. Keep the mypy configuration intact while mypy is still in parallel CI.
Pyrefly does not read the entry and uses its own native support. Remove
mypy-only configuration when the project actually retires mypy.
### Is native support guaranteed to produce identical types?
No. Pyrefly and mypy model these libraries independently. The goal is
high-quality framework-aware behavior, not identical internal types or
diagnostics.
## Related guides
- [Mypy vs Pyright vs Pyrefly](../../compare.mdx)
- [Migrate from mypy](index.mdx)
- [Mypy config reference](config-reference.mdx)
- [Pydantic support](../../pydantic.mdx)
- [Django support](../../django.mdx)
- [attrs support](../../attrs.mdx)
---
---
title: Mypy Strict Mode in Pyrefly
slug: /migrate/mypy/strict-mode
description:
Translate mypy strict mode into a reviewed Pyrefly configuration without
assuming that the two strict bundles are identical.
keywords:
- mypy strict
- mypy strict mode
- pyrefly strict
- mypy strict pyproject
---
{/*
* 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.
*/}
Mypy's `strict = true` and Pyrefly's `preset = "strict"` are both strictness
bundles, but they are not the same bundle, and each evolves on its own schedule.
Decide which of the two goals below you have before you configure anything.
## Goal 1: preserve the checks you have today
Let `pyrefly init` generate a mypy-oriented starting point, then review the
composite behavior explicitly:
```toml
preset = "legacy"
check-unannotated-defs = true
infer-return-types = "never"
```
Mypy strict includes `check_untyped_defs`, so `check-unannotated-defs = true` is
part of a faithful strict migration. The converter maps mypy's strictness flags
onto Pyrefly error kinds, but it does not expand the strict bundle into behavior
settings, and the [`legacy` preset](../../configuration.mdx#preset-legacy)
defaults `check-unannotated-defs` to `false`. Set it explicitly rather than
inheriting the preset default.
## Goal 2: adopt Pyrefly-native strictness
Once the migration is stable, this is a fresh policy decision rather than a
translation:
```toml
preset = "strict"
```
The [`strict` preset](../../configuration.mdx#preset-strict) sets
`strict-callable-subtyping = true` and enables
[`implicit-any`](../../error-kinds.mdx#implicit-any) (which covers every
implicit-`Any` sub-kind),
[`missing-override-decorator`](../../error-kinds.mdx#missing-override-decorator),
[`potential-bad-keyword-argument`](../../error-kinds.mdx#potential-bad-keyword-argument),
[`unused-ignore`](../../error-kinds.mdx#unused-ignore), and
[`direct-abstract-base-instantiation`](../../error-kinds.mdx#direct-abstract-base-instantiation).
## What the converter does with mypy strict
| Mypy strict behavior | Generated or closest Pyrefly policy | Review |
| ----------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------- |
| Returning `Any` from a typed function | `no-any-return = "error"` | Close mapping; Pyrefly splits implicit and explicit return `Any` internally |
| Redundant casts | `redundant-cast = "warn"` | Generated at warning severity |
| Untyped or incompletely typed definitions | `implicit-any-parameter` + `unannotated-return` | Two Pyrefly policies stand in for several mypy flags |
| Missing generic arguments | `implicit-any = "error"` | Broader than `implicit-any-type-argument` alone |
| Checking unannotated function bodies | Not expanded by the converter | Set `check-unannotated-defs = true` yourself |
| Explicit `Any` | `explicit-any` | Only when `disallow_any_explicit` is set; mypy `strict` alone does not enable it |
| Unused ignores | `unused-type-ignore` / `unused-ignore` | Not part of the mypy converter map; add by hand |
| Explicit override requirement | `missing-override-decorator` | Manual, unless you adopt the Pyrefly strict preset |
## A hand-tuned compatibility config
Use specific kinds when the umbrella is broader than you want:
```toml
preset = "legacy"
check-unannotated-defs = true
infer-return-types = "never"
[errors]
implicit-any-parameter = "error"
implicit-any-type-argument = "error"
unannotated-return = "error"
no-any-return = "error"
redundant-cast = "warn"
```
Then consider adding the policies mypy strict does not cover:
```toml
[errors]
missing-override-decorator = "error"
unused-type-ignore = "error"
```
## Related guides
- [Mypy vs Pyright vs Pyrefly](../../compare.mdx)
- [Migrate from mypy](index.mdx)
- [Mypy config reference](config-reference.mdx)
- [Mypy error codes](error-codes.mdx)
- [Pyrefly presets](../../configuration.mdx#preset)
---
---
title: Pyright Config to Pyrefly
slug: /migrate/pyright/config
description:
Translate pyrightconfig.json and [tool.pyright] settings into Pyrefly,
including execution environments and diagnostic rules.
keywords:
- pyright config
- pyrightconfig json
- tool.pyright
- convert pyright config 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.
*/}
`pyrefly init` converts the Pyright project, import, environment, and diagnostic
fields that have a Pyrefly counterpart. Not every Pyright setting has a precise
Pyrefly equivalent. Use this guide to understand the gaps and when you need to
update your config manually.
## Quick conversion
```sh
pyrefly init
```
Example source:
```json
{
"include": ["src"],
"exclude": ["**/generated"],
"extraPaths": ["src"],
"pythonVersion": "3.13",
"pythonPlatform": "Linux",
"reportMissingImports": "error",
"reportUnknownMemberType": "warning"
}
```
Representative Pyrefly result:
```toml
project-includes = ["src"]
project-excludes = ["**/generated"]
search-path = ["src"]
python-version = "3.13"
python-platform = "linux"
infer-with-first-use = false
[errors]
missing-import = "error"
unknown-attribute-type = "warn"
```
## Mapping status
- **Automatic:** `pyrefly init` reads the Pyright option and writes the Pyrefly
setting.
- **Manual:** a related Pyrefly setting exists, but `pyrefly init` doesn't
translate it, so you'll need to manually update your Pyrefly config.
- **Not mapped:** no current automatic or direct equivalent in Pyrefly.
## Project and type-evaluation settings
| Pyright option | Closest Pyrefly setting or workflow | Status | Migration note |
| ------------------------------ | ------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `include` | `project-includes` | Automatic | Paths and globs are carried over as-is. |
| `exclude` | `project-excludes` | Automatic | Review differences in default excludes and in files that are excluded but still imported. |
| `strict` | Explicit `[sub-config.errors]` overrides or separate configs | Manual | A Pyrefly preset cannot be selected inside a sub-config, so a path list cannot become `preset = "strict"` directly. |
| `extends` | Flatten the inherited config | Manual | Pyrefly config has no config-inheritance key. |
| `defineConstant` | No direct setting | Not mapped | Use ordinary Python constants or separate target configs. |
| `typeshedPath` | `typeshed-path` | Manual | The equivalent setting exists but is not read by the converter. |
| `stubPath` | `site-package-path` | Automatic | Review import precedence and whether the directory contains only stubs. |
| `venvPath` | Activated environment or `python-interpreter-path` | Manual | Pyrefly prefers querying the selected interpreter. |
| `venv` | Activated environment or `python-interpreter-path` | Manual | Pyrefly has no separate `venvPath` + `venv` pair. |
| `verboseOutput` | CLI logging | No 1:1 mapping | |
| `extraPaths` | `search-path` | Automatic | |
| `pythonVersion` | `python-version` | Automatic | |
| `pythonPlatform` | `python-platform` | Automatic | `All`, `Linux`, `Darwin`, and `Windows` are normalized. Pyrefly also accepts a list of target platforms. |
| `executionEnvironments` | `sub-config`, for diagnostic overrides only | Automatic; partial | Per-environment Python version, platform, and search paths cannot be represented in a Pyrefly sub-config. |
| `useLibraryCodeForTypes` | No direct setting | Not mapped | Validate behavior for untyped libraries. |
| `strictListInference` | No direct setting | Not mapped | Do not confuse this with `infer-with-first-use`, which controls unsolved types such as empty containers. |
| `strictDictionaryInference` | No direct setting | Not mapped | |
| `strictSetInference` | No direct setting | Not mapped | |
| `analyzeUnannotatedFunctions` | `check-unannotated-defs` | Manual | Both default to analyzing unannotated function bodies, but the converter does not read the Pyright field. |
| `strictParameterNoneValue` | No direct setting | Not mapped | |
| `enableTypeIgnoreComments` | `enabled-ignores` | Usually no change needed | Pyrefly respects `# type: ignore` by default. |
| `deprecateTypingAliases` | `deprecated` | Manual; not exact | |
| `enableReachabilityAnalysis` | `unreachable` / `unreachable-match-case` | Manual; not exact | Pyrefly has no global analysis toggle with the same semantics. |
| `enableExperimentalFeatures` | No direct setting | Not mapped | |
| `disableBytesTypePromotions` | No direct setting | Not mapped | |
| `typeCheckingMode` | `preset` | Manual; approximate | `off`, `basic`, and `strict` have similarly named Pyrefly presets, but the rule bundles are not identical. Pyright `standard` is closest to a reviewed Pyrefly default config. |
| `ignore` | Baseline, source suppressions, or project excludes | Manual; no exact 1:1 | Pyright suppresses diagnostics while still analyzing ignored files; `project-excludes` changes project membership. |
| `root` (execution environment) | `matches` in a `[[sub-config]]` | Automatic | Only the environment's diagnostic overrides are carried over. |
## Diagnostic settings
Recognized Pyright `report*` severities become Pyrefly `[errors]` entries.
Boolean `true` becomes `error`, `false` becomes `ignore`, and string levels
convert to the corresponding Pyrefly severity.
Because Pyright diagnostics are often umbrellas, one rule can become several
Pyrefly kinds. Use the complete [Pyright diagnostics](diagnostics-reference.mdx)
matrix rather than assuming the names are interchangeable.
## Execution environments
Source:
```json
{
"executionEnvironments": [
{
"root": "src/legacy",
"pythonVersion": "3.10",
"extraPaths": ["vendor"],
"reportUnknownMemberType": "none"
}
]
}
```
The diagnostic override becomes:
```toml
[[sub-config]]
matches = "src/legacy"
[sub-config.errors]
unknown-attribute-type = "ignore"
```
The per-environment Python version and import path are **not** preserved,
because Pyrefly sub-configs cannot override them. Use a separate configuration
file for a genuinely different Python environment.
## Path-specific strictness
Pyright's top-level `strict` array behaves like adding `# pyright: strict` to
every matched file. Pyrefly cannot put `preset = "strict"` inside a sub-config,
so translate the policy into explicit error severities:
```toml
[[sub-config]]
matches = "src/core/**"
[sub-config.errors]
implicit-any = "error"
missing-override-decorator = "error"
unused-ignore = "error"
```
Treat this as an example, not a complete recreation of Pyright strict mode.
## Linter-like Pyright rules
Several Pyright settings cover unused imports, duplicate imports, string
escapes, mutable default values, naming, and unused expressions. Pyrefly
deliberately does not map most of these checker-adjacent checks. Keep them in
Ruff or another linter rather than forcing them onto unrelated Pyrefly error
kinds.
## Upstream references
- [Pyright configuration](https://github.com/microsoft/pyright/blob/main/docs/configuration.md)
- [Pyrefly configuration](../../configuration.mdx)
## Next steps
- [Migrate from Pyright](index.mdx)
- [Pyright diagnostics](diagnostics-reference.mdx)
- [Pyright strict mode](strict-mode.mdx)
- [Pyrefly configuration](../../configuration.mdx)
:::note
Last reviewed against upstream Pyright documentation and Pyrefly's migration
source on 2026-08-08.
:::
---
---
title: Pyright Diagnostics to Pyrefly
slug: /migrate/pyright/diagnostics
description:
Map every upstream Pyright report rule to Pyrefly error kinds, including
automatic migration, split rules, and missing equivalents.
keywords:
- pyright diagnostics
- pyright report rules
- pyright error codes
- pyright to pyrefly mapping
---
{/*
* 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.
*/}
Pyright configs set severity policy with `report*` rules, in
`pyrightconfig.json`, `[tool.pyright]`, and execution environments. Pyrefly
expresses the same policy with [error kinds](../../error-kinds.mdx), which use
different names, so `pyrefly init` has to translate each rule as it converts the
config.
Some Pyright rules are not recognized by `pyrefly init` and are skipped without
a warning. Pyright rules are also frequently broader than Pyrefly kinds, so one
Pyright severity is often applied to several Pyrefly error kinds and is not
always an exact equivalent. Review the table below to compare Pyright rules and
Pyrefly error kinds.
## Severity conversion
| Pyright value | Pyrefly severity |
| --------------------------- | ---------------- |
| `false` or `"none"` | `ignore` |
| `"information"` or `"hint"` | `info` |
| `"warning"` | `warn` |
| `true` or `"error"` | `error` |
When several Pyright rules map onto the same Pyrefly kind, the highest severity
wins.
:::warning
A converted severity is not proof of exact diagnostic equivalence. One Pyright
rule may cover multiple scenarios, and Pyrefly may split those scenarios into
several kinds.
:::
## Complete upstream Pyright mapping
| Pyright diagnostic | Closest Pyrefly kind(s) | Config migration | Fidelity | Notes |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------- |
| `reportGeneralTypeIssues` | No single Pyrefly kind | Not mapped | Umbrella | Pyright combines many scenarios here; map the actual diagnostics to specific Pyrefly kinds. |
| `reportPropertyTypeMismatch` | `bad-assignment` and/or `read-only` | Manual | Related | No dedicated property getter/setter consistency rule. |
| `reportFunctionMemberAccess` | `missing-attribute` | Automatic | Direct-ish | |
| `reportMissingImports` | `missing-import` | Automatic | Direct | |
| `reportMissingModuleSource` | `missing-source`, `missing-source-for-stubs` | Automatic | Split | |
| `reportInvalidTypeForm` | `invalid-annotation`, `invalid-literal`, `invalid-type-alias`, `not-a-type` | Automatic | Split | Pyrefly uses more specific error kinds. |
| `reportMissingTypeStubs` | `untyped-import` | Automatic | Direct-ish | |
| `reportImportCycles` | No direct equivalent | Not mapped | None | Use architecture tooling or a dedicated import-cycle linter. |
| `reportUnusedImport` | No type-checker equivalent | Not mapped | Lint | Use Ruff `F401` or another linter. |
| `reportUnusedClass` | No type-checker equivalent | Not mapped | Lint | Use dead-code analysis where needed. |
| `reportUnusedFunction` | No type-checker equivalent | Not mapped | Lint | Use dead-code analysis where needed. |
| `reportUnusedVariable` | No type-checker equivalent | Not mapped | Lint | Use Ruff or another linter. |
| `reportDuplicateImport` | No type-checker equivalent | Not mapped | Lint | Use Ruff `F811` or another linter. |
| `reportWildcardImportFromLibrary` | No type-checker equivalent | Not mapped | Lint | Use Ruff `F403` or another linter. |
| `reportAbstractUsage` | `bad-instantiation` | Automatic | Partial | Pyright also covers abstract-method calls; Pyrefly uses the separate `abstract-method-call` kind for that scenario. |
| `reportArgumentType` | `bad-argument-type`, `invalid-argument` | Automatic | Split | |
| `reportAssertTypeFailure` | `assert-type` | Automatic | Direct | |
| `reportAssignmentType` | `bad-assignment`, `bad-unpacking`, `bad-typed-dict-key` | Automatic | Split | |
| `reportAttributeAccessIssue` | `missing-attribute`, `missing-module-attribute`, `no-access`, `read-only` | Automatic | Split | |
| `reportCallIssue` | `missing-argument`, `bad-argument-count`, `unexpected-positional-argument`, `unexpected-keyword`, `bad-keyword-argument`, `no-matching-overload`, `incompatible-overload-residual`, `not-callable` | Automatic | Split | This is one of the broadest Pyright diagnostics. |
| `reportInconsistentOverload` | `inconsistent-overload`, `invalid-overload` | Automatic | Split | |
| `reportIndexIssue` | `bad-index` | Automatic | Partial | Some unsupported indexing operations are reported as `unsupported-operation`. |
| `reportInvalidTypeArguments` | `bad-specialization` | Automatic | Direct-ish | |
| `reportNoOverloadImplementation` | `invalid-overload` | Automatic | Direct | |
| `reportOperatorIssue` | `unsupported-operation`, `invalid-argument`, `not-callable` | Automatic | Split | |
| `reportOptionalSubscript` | `unsupported-operation` | Automatic | Related | |
| `reportOptionalMemberAccess` | `missing-attribute` | Automatic | Related | |
| `reportOptionalCall` | `not-callable` | Automatic | Related | |
| `reportOptionalIterable` | `not-iterable` | Automatic | Direct-ish | |
| `reportOptionalContextManager` | `bad-context-manager` | Automatic | Direct-ish | |
| `reportOptionalOperand` | `unsupported-operation` | Automatic | Related | |
| `reportRedeclaration` | `redefinition` | Automatic | Direct-ish | |
| `reportReturnType` | `bad-return`, `invalid-yield` | Automatic | Split | |
| `reportTypedDictNotRequiredAccess` | `not-required-key-access` | Automatic | Direct | |
| `reportUntypedFunctionDecorator` | `untyped-function-decorator` | Automatic | Direct | |
| `reportUntypedClassDecorator` | `untyped-class-decorator` | Automatic | Direct | |
| `reportUntypedBaseClass` | `unsupported-dynamic-base` | Manual | Related | The Pyrefly kind is about dynamically unresolved bases, not every `Any`-typed base. |
| `reportUntypedNamedTuple` | No direct equivalent | Not mapped | None | |
| `reportPrivateUsage` | `no-access` | Automatic | Related | The tools differ in what they consider protected or private usage. |
| `reportTypeCommentUsage` | No direct equivalent | Not mapped | None | Use a linter if the goal is to prohibit legacy type comments. |
| `reportPrivateImportUsage` | `implicit-reexport` and/or `no-access` | Manual | Related | No one-to-one automatic mapping. |
| `reportConstantRedefinition` | `treat-all-caps-as-final = true` plus `bad-assignment` | Manual | Related | This changes inference policy rather than enabling a single diagnostic kind. |
| `reportDeprecated` | `deprecated` | Automatic | Direct | |
| `reportIncompatibleMethodOverride` | `bad-override` | Automatic | Broad | Pyrefly reports some cases under `bad-override-param-name`. |
| `reportIncompatibleVariableOverride` | `bad-override` | Automatic | Broad | Pyrefly reports some cases under `bad-override-mutable-attribute`. |
| `reportInconsistentConstructor` | No direct equivalent | Not mapped | None | |
| `reportOverlappingOverload` | `inconsistent-overload` | Manual | No exact 1:1 | |
| `reportPossiblyUnboundVariable` | `unbound-name` | Automatic | Direct-ish | |
| `reportMissingSuperCall` | `missing-super-call` | Manual | Direct | The Pyrefly kind exists but is not part of automatic config migration. |
| `reportUninitializedInstanceVariable` | `implicitly-defined-attribute` | Automatic | Related | The exact initialization analysis differs. |
| `reportInvalidStringEscapeSequence` | No type-checker equivalent | Not mapped | Lint | Use Ruff `W605` or Python warnings. |
| `reportUnknownParameterType` | `implicit-any-parameter` | Automatic | Direct-ish | |
| `reportUnknownArgumentType` | `unknown-argument-type` | Automatic | Direct | |
| `reportUnknownLambdaType` | `implicit-any-lambda` | Automatic | Direct-ish | |
| `reportUnknownVariableType` | `unknown-variable-type` | Automatic | Direct | |
| `reportUnknownMemberType` | `unknown-attribute-type` | Automatic | Direct-ish | |
| `reportMissingParameterType` | `implicit-any-parameter` | Automatic | Direct-ish | |
| `reportMissingTypeArgument` | `implicit-any-type-argument` | Automatic | Direct-ish | |
| `reportInvalidTypeVarUse` | `invalid-type-var` | Automatic | Broad | Some variance-specific cases use `invalid-variance` or `variance-mismatch`. |
| `reportCallInDefaultInitializer` | No type-checker equivalent | Not mapped | Lint | Use Ruff `B006` or another linter. |
| `reportUnnecessaryIsInstance` | `redundant-condition` | Automatic | Related | |
| `reportUnnecessaryCast` | `redundant-cast` | Automatic | Direct | |
| `reportUnnecessaryComparison` | `incompatible-comparison`, `unnecessary-comparison` | Automatic | Split | |
| `reportUnnecessaryContains` | No direct equivalent | Not mapped | None | |
| `reportAssertAlwaysTrue` | `redundant-condition` | Manual | Related | No dedicated tuple-assert rule. |
| `reportSelfClsParameterName` | No type-checker equivalent | Not mapped | Lint | Use Ruff `N804` / `N805`. |
| `reportImplicitStringConcatenation` | No type-checker equivalent | Not mapped | Lint | Use Ruff `ISC001` or another linter. |
| `reportUndefinedVariable` | `unknown-name` | Automatic | Direct | |
| `reportUnboundVariable` | `unbound-name` | Automatic | Direct | |
| `reportUnhashable` | No direct equivalent | Not mapped | None | |
| `reportInvalidStubStatement` | No type-checker equivalent | Not mapped | Lint | Use Ruff's PYI rules where applicable. |
| `reportIncompleteStub` | No direct equivalent | Not mapped | None | |
| `reportUnsupportedDunderAll` | `bad-dunder-all`, `unresolvable-dunder-all` | Automatic | Split | |
| `reportUnusedCallResult` | `unused-call-result` | Manual | Direct-ish | The Pyrefly kind exists but is not part of automatic config migration. |
| `reportUnusedCoroutine` | `unused-coroutine` | Automatic | Direct | |
| `reportUnusedExcept` | No direct equivalent | Not mapped | None | |
| `reportUnusedExpression` | No type-checker equivalent | Not mapped | Lint | Use Ruff `B018` or another linter. |
| `reportUnnecessaryTypeIgnoreComment` | `unused-type-ignore` and/or `unused-ignore` | Manual | Direct-ish | Select the Pyrefly kind based on whether you are checking `# type: ignore` or tool-specific ignores. |
| `reportMatchNotExhaustive` | `non-exhaustive-match` | Automatic | Direct | |
| `reportUnreachable` | `unreachable`, `unreachable-match-case` | Automatic | Split | |
| `reportImplicitOverride` | `missing-override-decorator` | Automatic | Direct | |
## Why `reportGeneralTypeIssues` has no single mapping
Pyright uses `reportGeneralTypeIssues` for many checks that are not assigned to
a narrower `report*` rule. Depending on the scenario, Pyrefly might emit:
- `invalid-inheritance`;
- `invalid-annotation`;
- `bad-raise`;
- `not-iterable`;
- `bad-context-manager`;
- `bad-match`;
- `invalid-super-call`;
- another specific error kind.
A global `reportGeneralTypeIssues = "none"` cannot be translated safely into one
Pyrefly suppression. Run the code and decide which concrete Pyrefly kinds, if
any, should be disabled.
## BasedPyright-only settings
BasedPyright adds rules beyond upstream Pyright. `pyrefly init` recognizes some
of them:
| BasedPyright rule | Pyrefly |
| --------------------------------------- | -------------------------------- |
| `reportExplicitAny` | `explicit-any` |
| `reportImplicitRelativeImport` | `missing-import` |
| `reportIncompatibleUnannotatedOverride` | `bad-override-mutable-attribute` |
These are a separate compatibility layer; an upstream Pyright config will not
contain them.
## Related guides and references
- [Migrate from Pyright](index.mdx)
- [Pyright config reference](config-reference.mdx)
- [`reportMissingImports`](missing-imports.mdx)
- [`reportUnknownMemberType`](unknown-member-type.mdx)
- [Pyrefly error kinds](../../error-kinds.mdx)
- [Pyright configuration and diagnostic rules](https://github.com/microsoft/pyright/blob/main/docs/configuration.md)
:::note
Last reviewed against upstream Pyright configuration documentation and Pyrefly's
migration source on 2026-08-08.
:::
---
---
title: Migrate from Pyright
slug: /migrating-from-pyright
description:
'Move a Python project from Pyright or Pylance to Pyrefly: install
alongside, convert pyrightconfig.json, review diagnostic mappings, and roll
out with suppressions or a baseline.'
keywords:
- migrate from pyright
- pyright to pyrefly
- pyright alternative
- pylance alternative
- pyrightconfig migration
---
{/*
* 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.
*/}
For most projects migrating from Pyright to Pyrefly is fairly simple. Install
Pyrefly, use `pyrefly init` to convert `pyrightconfig.json` and review what it
produced. You do not need to change editor defaults or delete the JSON file to
get started.
If your project uses execution environments or sets
`typeCheckingMode = "strict"`, follow this page first and then read the
[linked guide](#going-further) for that case.
## 1. Install Pyrefly and convert your config
```sh
# uv
uv add --dev pyrefly
# pip
python -m pip install pyrefly
```
```sh
pyrefly init
```
That is the migration. `pyrefly init` finds your `pyrightconfig.json` or
`[tool.pyright]` section and writes the equivalent `pyrefly.toml` or
`[tool.pyrefly]`, preserving `include` and `exclude`, `extraPaths`, `stubPath`,
`pythonVersion` and `pythonPlatform`, the recognized `report*` diagnostic
severities, and the diagnostic overrides inside execution environments. Pass a
path to migrate a project other than the current directory, and see
`pyrefly init --help` for more options.
Your `pyrightconfig.json` is left in place. Pyrefly only reads its own config
once one exists, so both checkers keep working and you can back out at any
point.
## 2. Check the config was mapped correctly
Read the generated config line by line before committing it. Some configs may
not have been mapped exactly. `extends`, virtual-environment selection,
`typeshedPath`, `defineConstant`, and the strict inference toggles are not
preserved automatically. See the
[Pyright config reference](config-reference.mdx) to check if any of your config
values were missed and need manual adjustment.
Alongside your converted settings, `pyrefly init` writes:
```toml
infer-with-first-use = false
```
so that empty containers and otherwise unsolved type variables behave more like
Pyright, which infers `Any` for them. Remove the setting only after reviewing
the resulting inference changes.
### Diagnostic severities
Many Pyright rules are umbrellas that fan out to several Pyrefly error kinds, so
one migrated severity enables or disables several kinds at once:
| Pyright rule | Pyrefly kinds |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reportCallIssue` | `missing-argument`, `bad-argument-count`, `unexpected-positional-argument`, `unexpected-keyword`, `bad-keyword-argument`, `no-matching-overload`, `incompatible-overload-residual`, `not-callable` |
| `reportAttributeAccessIssue` | `missing-attribute`, `missing-module-attribute`, `no-access`, `read-only` |
| `reportInvalidTypeForm` | `invalid-annotation`, `invalid-literal`, `invalid-type-alias`, `not-a-type` |
| `reportAssignmentType` | `bad-assignment`, `bad-unpacking`, `bad-typed-dict-key` |
| `reportReturnType` | `bad-return`, `invalid-yield` |
Broad `none` and `false` settings deserve the most attention, since they switch
off more in Pyrefly than they did in Pyright. The
[Pyright diagnostic reference](diagnostics-reference.mdx) has the complete
mapping, along with the severity conversion table and the BasedPyright rules the
converter also recognizes.
Pyright's linter-like rules, covering unused imports, duplicate imports,
implicit string concatenation, mutable defaults, and naming, have no Pyrefly
equivalent. Keep those with Ruff or another linter.
### Execution environments
This is the one place a Pyright project can be structurally incompatible with a
single Pyrefly config. An execution environment can change import search paths,
the Python version, the Python platform, and diagnostic severities. Pyrefly
[sub-configs](../../configuration.mdx#sub-configs) carry only a limited set of
path-specific overrides, so the converter turns the diagnostic overrides into
sub-configs and drops the rest.
Go through the environments in your `pyrightconfig.json` one at a time and
compare each against the generated config. Where two environments differ only in
diagnostic severities, sub-configs cover it. Where they differ in Python
version, platform, or search paths, they cannot be expressed in one config, so
give each subproject its own Pyrefly configuration file.
## 3. Handle the new errors and drop Pyright
With the config settled, run both checkers on the same commit and compare:
```sh
pyright
pyrefly check
```
You may still find some differences in error output as Pyrefly implements the
type system independently of Pyright and will not reproduce its diagnostics
exactly.
Classify each difference rather than counting them: a true bug, an inference
difference, a framework behavior gap, a config gap, or a Pyright-specific lint.
Pyrefly and Pyright often describe the same scenario at different levels of
granularity, so the goal is equivalent policy rather than identical labels.
Include framework-heavy code in the comparison. Pyrefly models
[Pydantic](../../pydantic.mdx), [Django](../../django.mdx), and
[attrs](../../attrs.mdx) inside the checker and the language server rather than
relying on stubs alone, so generated model constructors, ORM fields and
relationships, validation modes, converters, and editor autocomplete are worth
testing directly.
Pyright automatically imports builtins defined in `__builtins__.pyi` at the
project root or in the `stubPath` directory, which defaults to `./typings`.
Pyrefly supports this too: the `stubPath` directory needs to appear in
`site-package-path`, which is where `pyrefly init` puts it.
Once you know what the differences are, decide how to absorb the ones you are
not fixing up front. Start with suppressions. `pyrefly suppress` inserts
comments across the project for you, and `# pyrefly: ignore[bad-assignment]`
silences a single line:
```sh
pyrefly suppress
```
The comments live next to the code they apply to, so the remaining work stays
visible and can be cleared file by file. If Pyright already places its own
comments on the line before an error, use
`pyrefly suppress --comment-location=same-line` to avoid conflicts.
If your codebase is large enough that suppression comments would mean churn
across thousands of lines, use a
[baseline](../../error-suppressions.mdx#baseline-files-experimental) instead:
```sh
pyrefly check --baseline="pyrefly-baseline.json" --update-baseline
pyrefly check --baseline="pyrefly-baseline.json"
```
A baseline records the current errors in a file rather than in your source, and
blocks only newly introduced errors while still showing the suppressed
diagnostics in the IDE.
Whichever you choose, Pyrefly respects `# type: ignore` by default. To also
honor Pyright's ignores during the transition:
```toml
enabled-ignores = ["type", "pyright", "pyrefly"]
```
Keep that temporary and remove obsolete Pyright ignores once the migration
stabilizes. See the [error suppression docs](../../error-suppressions.mdx).
Once CI is green on Pyrefly alone and the team is happy with the editor
experience, drop Pyright: remove it from your dependencies and CI, delete
`pyrightconfig.json` or the `[tool.pyright]` section, and narrow
`enabled-ignores` back to `["type", "pyrefly"]`. Then clear the suppressions or
the baseline as you go with `pyrefly suppress --remove-unused`.
## Going further
| If your project | Read |
| ---------------------------------------------------------- | ---------------------------------------------------- |
| Sets `typeCheckingMode = "strict"` or a `strict` path list | [Pyright strict mode](strict-mode.mdx) |
| Reports `reportMissingImports` | [`reportMissingImports`](missing-imports.mdx) |
| Reports `reportUnknownMemberType` | [`reportUnknownMemberType`](unknown-member-type.mdx) |
| Needs the full option-by-option mapping | [Pyright config reference](config-reference.mdx) |
| Needs the full diagnostic mapping | [Pyright diagnostics](diagnostics-reference.mdx) |
If a specific diagnostic, execution-environment setup, or configuration option
is what stops you from adopting Pyrefly, please
[open an issue](https://github.com/facebook/pyrefly/issues).
---
---
title: Pyright reportMissingImports in Pyrefly
slug: /migrate/pyright/missing-imports
description:
Map Pyright reportMissingImports to Pyrefly missing-import and fix
interpreter, search-path, stub, and optional dependency issues.
keywords:
- reportMissingImports
- pyright missing imports
- pyright import could not be resolved
- pyrefly missing import
---
{/*
* 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.
*/}
Pyright's `reportMissingImports` fires when no source file or stub can be
resolved for an import. The direct Pyrefly equivalent is the
[`missing-import`](../../error-kinds.mdx#missing-import) error kind:
```toml
[errors]
missing-import = "error"
```
`pyrefly init` converts the Pyright severity automatically, so an existing
`reportMissingImports` policy carries over.
## Fix the environment before suppressing the error
An unresolved import usually means the environment, not the code, is wrong.
Check these in order:
1. **Interpreter:** is Pyrefly querying the environment where the package is
installed?
2. **Project roots:** should the source directory be in `search-path`?
3. **Custom stubs:** should a stub directory be in `site-package-path`?
4. **Generated code:** has the module been generated before type checking runs?
5. **Optional dependency:** is the import intentionally unavailable in some
installations?
If the module resolves from the command line but not in the IDE, or the other
way round, the two are using different configs or different interpreters. Check
which `pyrefly.toml` governs the file and what `python-interpreter-path` it
resolves to.
## Equivalent import settings
| Pyright | Pyrefly |
| --------------------------- | --------------------------------------------- |
| `extraPaths` | `search-path` |
| `stubPath` | `site-package-path` |
| `pythonVersion` | `python-version` |
| `pythonPlatform` | `python-platform` |
| `reportMissingImports` | `missing-import` |
| `reportMissingModuleSource` | `missing-source` + `missing-source-for-stubs` |
| `reportMissingTypeStubs` | `untyped-import` |
## Optional dependencies
For selected modules that may legitimately be absent:
```toml
ignore-missing-imports = ["optional_vendor.*"]
```
This substitutes `Any` only when the matched module cannot be found; if it is
found, its real types are used. Prefer it to disabling `missing-import`
globally, which also hides genuinely broken imports.
## Source versus stubs
Pyright distinguishes three situations: the module is missing entirely, stubs
are found but the runtime source is missing, and source is found but type stubs
are missing. Pyrefly keeps the same distinction across `missing-import`,
`missing-source`, `missing-source-for-stubs`, and `untyped-import`. Preserve
those separate severities rather than collapsing them into a single ignore.
## Related guides
- [Migrate from Pyright](index.mdx)
- [Pyright config reference](config-reference.mdx)
- [Pyright diagnostics](diagnostics-reference.mdx)
- [Import resolution](../../import-resolution.mdx)
---
---
title: Pyright Strict Mode in Pyrefly
slug: /migrate/pyright/strict-mode
description:
Translate Pyright strict type checking into Pyrefly presets and explicit
error severities, including path-specific strict rules.
keywords:
- pyright strict
- pyright strict mode
- typeCheckingMode strict
- pyrefly strict
---
{/*
* 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.
*/}
Pyright strict mode and Pyrefly strict mode are both useful policy bundles, but
they do not enable the same checks. Treat "strict" as a name for two different
policies rather than a setting to copy across.
## Global strict mode
Pyright:
```json
{
"typeCheckingMode": "strict"
}
```
Pyrefly starting point:
```toml
preset = "strict"
infer-with-first-use = false
```
`infer-with-first-use = false` keeps empty-container and unsolved-type-variable
inference closer to Pyright; it is also what `pyrefly init` writes when it
converts a Pyright config. Then review your explicit `report*` overrides using
the [diagnostic mapping](diagnostics-reference.mdx).
## Why the presets differ
Pyright strict mode enables its own collection of `report*` rules, including
several linter-like checks. Pyrefly's
[`strict` preset](../../configuration.mdx#preset-strict) instead sets
`strict-callable-subtyping = true` and enables these error kinds:
- [`implicit-any`](../../error-kinds.mdx#implicit-any), which covers every
implicit-`Any` sub-kind;
- [`missing-override-decorator`](../../error-kinds.mdx#missing-override-decorator);
- [`potential-bad-keyword-argument`](../../error-kinds.mdx#potential-bad-keyword-argument);
- [`unused-ignore`](../../error-kinds.mdx#unused-ignore);
- [`direct-abstract-base-instantiation`](../../error-kinds.mdx#direct-abstract-base-instantiation).
Keep Ruff or another linter for the Pyright rules about unused imports,
duplicate imports, implicit string concatenation, mutable defaults, and naming.
Pyrefly does not try to absorb them.
## Path-specific strict mode
Pyright:
```json
{
"strict": ["src/core"]
}
```
Pyrefly cannot put `preset = "strict"` inside a `[[sub-config]]`, so encode the
policies you care about explicitly:
```toml
[[sub-config]]
matches = "src/core/**"
[sub-config.errors]
implicit-any = "error"
missing-override-decorator = "error"
unused-ignore = "error"
potential-bad-keyword-argument = "error"
```
The result is an explicit policy you can extend after comparing both tools,
rather than a recreation of Pyright strict mode. If a subtree also needs a
different Python version, platform, or import roots, give it its own
configuration file instead.
## Broad Pyright rules still need review
Strict mode turns on broad rules such as `reportCallIssue`,
`reportAttributeAccessIssue`, and `reportGeneralTypeIssues`. Pyrefly splits the
first two into many specific kinds and has no single equivalent for the last
one, so review the generated `[errors]` table rather than trusting the word
"strict".
## Related guides
- [Mypy vs Pyright vs Pyrefly](../../compare.mdx)
- [Migrate from Pyright](index.mdx)
- [Pyright config reference](config-reference.mdx)
- [Pyright diagnostics](diagnostics-reference.mdx)
- [Pyrefly presets](../../configuration.mdx#preset)
---
---
title: Pyright reportUnknownMemberType in Pyrefly
slug: /migrate/pyright/unknown-member-type
description:
Map Pyright reportUnknownMemberType to Pyrefly unknown-attribute-type and
distinguish unknown member types from missing attributes.
keywords:
- reportUnknownMemberType
- pyright unknown member type
- pyright unknown attribute
- unknown-attribute-type
---
{/*
* 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.
*/}
Pyright's `reportUnknownMemberType` fires when a member exists but its type
cannot be determined. `pyrefly init` maps it to
[`unknown-attribute-type`](../../error-kinds.mdx#unknown-attribute-type):
```toml
[errors]
unknown-attribute-type = "error"
```
The Pyrefly kind defaults to `ignore`, so a project that relied on Pyright's
strict unknown-member policy has to enable it explicitly. `pyrefly init` does
that for you when the Pyright config sets the rule.
## Unknown is not the same as missing
```python
class Dynamic:
value = some_untyped_factory()
item = Dynamic()
item.value # the member exists, but its type is unknown
item.does_not_exist # the member is missing
```
Pyrefly reports these as different kinds:
| Scenario | Pyrefly kind |
| --------------------------------------------------------------------------------------------------- | -------------------------- |
| The attribute exists but its inferred type is unknown | `unknown-attribute-type` |
| The attribute does not exist | `missing-attribute` |
| An imported name is absent from a module | `missing-module-attribute` |
| The attribute is implicitly `Any`-like because it was assigned `None` or `()` without an annotation | `implicit-any-attribute` |
Substituting `missing-attribute` for `reportUnknownMemberType` changes the
policy rather than preserving it.
## Related unknown-type rules
| Pyright | Pyrefly |
| ---------------------------- | ------------------------ |
| `reportUnknownMemberType` | `unknown-attribute-type` |
| `reportUnknownVariableType` | `unknown-variable-type` |
| `reportUnknownArgumentType` | `unknown-argument-type` |
| `reportUnknownParameterType` | `implicit-any-parameter` |
| `reportUnknownLambdaType` | `implicit-any-lambda` |
## Reduce unknown member types at the source
- install or improve package stubs;
- annotate factory return types;
- annotate class attributes explicitly;
- avoid replacing resolvable imports with `Any`;
- review decorators or framework behavior that generates members dynamically.
If the project previously relied on Pylance- or Pyright-specific behavior for
generated members, exercise those frameworks in Pyrefly before weakening the
diagnostic. Pyrefly models [Pydantic](../../pydantic.mdx),
[Django](../../django.mdx), and [attrs](../../attrs.mdx) directly, so an unknown
member on a model or an ORM field is more likely a framework-support gap worth
reporting than a reason to disable the rule project-wide.
## Related guides
- [Migrate from Pyright](index.mdx)
- [Pyright diagnostics](diagnostics-reference.mdx)
- [Pyrefly error kinds](../../error-kinds.mdx)
---
---
title: Pants Integration
slug: /pants
description: Run Pyrefly type checking over Pants Python targets with the pants-pyrefly plugin
---
{/*
* 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.
*/}
# Pants Integration
The [Pants](https://www.pantsbuild.org/) integration is provided by the
community plugin [`pants-pyrefly`](https://github.com/tague/pants-pyrefly)
(available on [PyPI](https://pypi.org/project/pants-pyrefly/); check there for
the latest version). Installing it makes Pyrefly a checker in the Pants
[`check`](https://www.pantsbuild.org/stable/docs/python/goals/check) goal, so
`pants check ::` type-checks your repository.
Pants downloads the official prebuilt Pyrefly binary (pinned by SHA256) and runs
it hermetically in a sandbox. Instead of maintaining a `pyrefly.toml`
`search-path` by hand, the plugin derives Pyrefly's first-party search paths
from your Pants source roots and points Pyrefly at a venv built from each
target's resolved third-party requirements, so imports resolve the same way they
do at runtime.
:::info Community-maintained
Pants support is not built into Pyrefly itself, and Meta does not publish an
official `pants.backend.pyrefly`. The plugin is maintained by the community, so
its release cadence and Pants version support are set there rather than by
Pyrefly.
:::
## When to use it
- Your Python code is already built with Pants and you want `pants check` to
type-check it with Pyrefly, using the same source roots, resolves, and
interpreter constraints Pants already knows about.
- You are moving a Pants repo off mypy and want the migration to happen through
Pants — config conversion, incremental adoption, and CI gating — rather than
as a separate out-of-band `pyrefly` invocation.
If you do not use Pants, use [`pyrefly check`](./configuration.mdx) as before.
Unlike the [Bazel integration](./bazel.mdx), the Pants plugin *does* read
`pyrefly.toml` (or `pyproject.toml` `[tool.pyrefly]`): strictness and
per-directory policy stay in your Pyrefly config, while the plugin supplies the
build-graph facts — search paths, interpreter, and the file list — plus a few
reporting overrides (`output_format`, `min_severity`, `only`, `baseline`) that
it passes on the command line, where they take precedence over the config file.
## Setup
### 1. Install the plugin
Add the plugin and enable its backend in `pants.toml`. Keep your existing
`pants.backend.python` entry; `pants_pyrefly` is added alongside it:
```toml
[GLOBAL]
plugins = ["pants-pyrefly==PANTS_PYREFLY_VERSION"]
backend_packages.add = [
"pants.backend.python",
"pants_pyrefly",
]
```
Replace `PANTS_PYREFLY_VERSION` with the latest
[release](https://pypi.org/project/pants-pyrefly/). The published wheel is pure
Python and declares no `pantsbuild.pants` dependency, so one release installs
into any supported Pants version; see the plugin's
[compatibility table](https://github.com/tague/pants-pyrefly#pants-compatibility)
for the supported Pants range and the Pyrefly version each plugin release pins.
To pin a different Pyrefly than the plugin's default, set `[pyrefly].version`
(with matching `[pyrefly].known_versions`).
To vendor the plugin instead — for rapid iteration or to pin to an exact source
state — copy `pants-plugins/pants_pyrefly/` into your repo and load it the way
in-repo plugins are normally loaded:
```toml
[GLOBAL]
pythonpath = ["%(buildroot)s/pants-plugins"]
backend_packages.add = ["pants.backend.python", "pants_pyrefly"]
```
If you keep plugin code in its own resolve, add it there and run
`pants generate-lockfiles`.
### 2. Create a Pyrefly config
`pants pyrefly-init` wraps [`pyrefly init`](./configuration.mdx). Running it
through Pants means the plugin supplies the Pyrefly binary, so you do not need
Pyrefly installed separately:
```shell
pants pyrefly-init
```
If your build root has a `pyproject.toml`, this adds a `[tool.pyrefly]` table to
it; otherwise it creates a `pyrefly.toml`. Either way it refuses to overwrite an
existing Pyrefly config — remove that first to regenerate.
**Migrating an existing checker?** If the repo has a mypy configuration, `init`
translates it instead of writing defaults; go to
[Bootstrapping from Mypy](#bootstrapping-a-configuration-from-mypy) below and
run that section's command *instead* of the bare `pants pyrefly-init` above,
since `init` will not overwrite the config a plain run just wrote. Pyright
configurations are also migrated, but only from `[tool.pyright]` in
`pyproject.toml`: the plugin does not stage `pyrightconfig.json` into the
sandbox, so a config in that file is invisible to `pyrefly-init`.
Checking policy — strictness, per-directory overrides, error suppression — is
configured here as documented in [Configuration](./configuration.mdx), not on
the plugin. The plugin discovers this file automatically; use `[pyrefly].config`
to point at a non-standard path.
### 3. Run
```shell
# one subtree
pants check path/to/dir::
# whole repo
pants check ::
```
Diagnostics are printed in Pyrefly's usual format, and anything at or above the
reporting threshold — `error` by default, or whatever `[pyrefly].min_severity`
sets — fails the goal. Targets are partitioned by resolve and interpreter
constraints, and each partition gets its own Pyrefly invocation against the
interpreter and requirements for that partition. If Pyrefly itself fails (an
exit code other than "clean" or "found errors"), the plugin reports that as a
tool failure rather than as type errors.
If more than one checker is enabled (during a mypy transition, for example),
scope the goal to one of them. `--only` belongs to `check`, so it goes after the
goal name:
```shell
pants check --only=pyrefly ::
```
### 4. Configure your editor
Pyrefly ships an [LSP server](./IDE.mdx), but on its own it does not know your
Pants source roots, so first-party imports will not resolve. Write them into
your Pyrefly config as `search-path`, along with the `python-version` your
interpreter constraints imply:
```shell
pants pyrefly-lsp-config
```
For third-party imports, point your editor's interpreter at an exported venv
(`pants export --resolve=python-default`). If your Pyrefly config lives in
`pyproject.toml` under `[tool.pyrefly]`, the goal prints the keys to add rather
than writing a `pyrefly.toml` that would shadow it.
:::note
Pyrefly *adds* a config file's `search-path` to whatever is passed on the
command line rather than replacing it, so the roots written here are also in
effect during `pants check`. That is usually harmless, but if one of them is the
build root (`.`), it re-exposes every file under a second module name inside the
sandbox — the exact problem the staging described in
[How it works](#how-it-works) exists to prevent. If `check` starts reporting
duplicate-module errors right after you run this goal, that is the cause.
:::
## Configuration
Options live in the `[pyrefly]` subsystem in `pants.toml`, and each has a
corresponding command-line flag:
| Option | Description |
| --- | --- |
| `skip` | Don't run Pyrefly during `check`. |
| `args` | Extra arguments passed to Pyrefly, e.g. `--pyrefly-args='--python-version 3.12'`. |
| `output_format` | Override Pyrefly's output format — any value [`--output-format`](./configuration.mdx#output-format) accepts, e.g. `json`, `github` (GitHub Actions annotations), `junit-xml`, `sarif`. |
| `min_severity` | Only report errors at or above this severity: `ignore`, `info`, `warn`, `error`. |
| `only` | Only report these [error kinds](./error-kinds.mdx), e.g. `bad-assignment`. Useful for triaging one category at a time. |
| `config` / `config_discovery` | Point at a Pyrefly config in a non-standard location, or turn off automatic discovery. |
| `baseline` | Path to a Pyrefly baseline JSON; `check` then reports only errors new since the baseline. |
| `extra_type_stubs` | Stub-only packages to add to the type-check environment without making them runtime dependencies, e.g. `types-requests`. Resolved directly rather than from a lockfile, so pin versions. |
| `version` / `known_versions` / `url_template` | Pin or override the downloaded Pyrefly binary. |
Opt an individual target out of Pyrefly with the `skip_pyrefly` field:
```python
python_sources(skip_pyrefly=True)
```
`config`, `config_discovery`, `extra_type_stubs`, and the binary-pinning options
are advanced; `pants help-advanced pyrefly` lists them alongside
`[pyrefly].interpreter_constraints`, the fallback used for targets that declare
none of their own. See also the plugin's
[Configuration section](https://github.com/tague/pants-pyrefly#configuration).
## Bootstrapping a configuration from Mypy
Pyrefly can read an existing mypy configuration and produce an equivalent
`pyrefly.toml`, so you start from your repo's established strictness and
per-module policy rather than from Pyrefly's defaults.
The migration reads mypy's own config files: `mypy.ini`, or `[tool.mypy]` in
`pyproject.toml`. Two things it will not pick up:
- **`setup.cfg`.** Pyrefly's migration does not read it; move a `[mypy]` section
there into `mypy.ini` first.
- **Pants's mypy subsystem options.** `[mypy].args` and friends in `pants.toml`
are Pants configuration rather than mypy configuration. If that is where all
your mypy settings live, there is nothing on disk to migrate and you should
configure Pyrefly from scratch instead.
### 1. Install alongside Mypy
Follow [Install the plugin](#1-install-the-plugin) above, but keep the mypy
backend enabled during the transition so you can run both checkers and compare
their output:
```toml
[GLOBAL]
backend_packages.add = [
"pants.backend.python",
"pants.backend.python.typecheck.mypy", # keep during the transition
"pants_pyrefly",
]
```
`pants check ::` now runs both. Scope to one with `check`'s `--only`:
```shell
pants check --only=pyrefly ::
pants check --only=mypy ::
```
### 2. Convert the config
```shell
pants pyrefly-init --pyrefly-init-migrate-from=mypy
```
This runs `pyrefly init --migrate-from mypy` and writes the translated settings
to `pyrefly.toml`. Passing `mypy` explicitly forces mypy as the source; with no
flag, `init` auto-detects (mypy, then Pyright).
When `init` detects a mypy config it selects the
[`legacy` preset](./configuration.mdx#preset-legacy), which turns off a few
checks mypy does not have so the first Pyrefly run does not bury you in error
classes mypy never flagged. Behavior still differs between the two checkers; the
preset narrows the gap rather than closing it.
Review the generated config — the mapping is close but not one-to-one. Broad
settings like `ignore_missing_imports`, per-module overrides, and strictness
flags carry over. **mypy plugins do not**: Pyrefly has no plugin system, so a
`plugins = ` entry is dropped.
For most of the widely used mypy plugins this does not matter, because Pyrefly
supports the same libraries natively and automatically —
[Pydantic](./pydantic.mdx), [Django](./django.mdx)'s ORM, [attrs](./attrs.mdx),
and [DataFrames](./dataframes.mdx). Where a plugin has no native counterpart —
SQLAlchemy's declarative-ORM plugin is the common case, and Pydantic v1, which
Pyrefly deliberately does not support — the dynamic attributes it synthesized
will surface as errors, and stub packages such as `sqlalchemy2-stubs` only
partly bridge the gap. For those packages, either suppress the errors, fold them
into a baseline, or keep mypy enabled for just those packages during the
transition.
[Migrating from Mypy](./migrate/mypy/index.mdx) covers the checker-level
differences — command-line equivalents, suppression syntax, and behavioral
divergences — independently of Pants.
### 3. Adopt incrementally with a baseline
A large codebase will have pre-existing errors under a stricter configuration.
Record them in a
[baseline](./error-suppressions.mdx#baseline-files-experimental) so `check` only
fails on *new* ones. Set the path in `pants.toml` — the plugin materializes the
baseline into its sandbox only when `[pyrefly].baseline` names it, so a
`baseline` key in `pyrefly.toml` has no effect here — and commit the file:
```toml
[pyrefly]
baseline = "build-support/pyrefly-baseline.json"
```
```shell
pants pyrefly-update-baseline :: # record current errors
pants check :: # now reports only errors introduced since
```
Re-run `pants pyrefly-update-baseline` as you fix errors to ratchet the baseline
down.
Prefer inline suppressions? `pants pyrefly-suppress ::` rewrites the targeted
files in place, adding a `# pyrefly: ignore` comment on the line above each
error (see [Error Suppressions](./error-suppressions.mdx)); delete them as you
fix, or run `pants pyrefly-suppress --pyrefly-suppress-remove-unused ::` to
strip stale ones. It edits your working tree, so run it on a clean checkout. A
baseline file and inline suppressions solve the same problem — pick one.
### 4. Track progress
`pants pyrefly-coverage` reports the share of typable symbols that have a
non-`Any` type — Pyrefly's **strict** coverage, not the looser metric that
counts `Any` as covered (see [Measuring Type Coverage](./report.mdx) for both).
It works well as a migration ratchet in CI, though its threshold is therefore
not comparable to a `pyrefly coverage check --fail-under` number:
```shell
pants pyrefly-coverage ::
pants pyrefly-coverage --pyrefly-coverage-fail-under=80 ::
```
### 5. Remove Mypy
Once Pyrefly is green (or a baseline established) and CI gates on it, drop
`pants.backend.python.typecheck.mypy` from `backend_packages` and delete the
mypy configuration.
## Goals
Besides participating in `check`, the plugin adds these goals:
| Goal | Purpose |
| --- | --- |
| `pyrefly-init` | Bootstrap `pyrefly.toml`, migrating a mypy or Pyright config when present. |
| `pyrefly-lsp-config` | Write Pants's source roots into `pyrefly.toml` as `search-path` for the IDE/LSP. |
| `pyrefly-update-baseline` | Record current errors in the baseline named by `[pyrefly].baseline`. |
| `pyrefly-suppress` | Add (or, with `--remove-unused`, strip) inline `# pyrefly: ignore` comments. |
| `pyrefly-coverage` | Report type coverage, optionally failing under a threshold. |
| `pyrefly-dump-config` | Print the effective Pyrefly configuration Pants assembles. Diagnostic only. |
## How it works
Each partition of targets — grouped by resolve and interpreter constraints —
gets one Pyrefly invocation over that partition's sources.
- **First-party code** resolves through `--search-path`, the analogue of
`MYPYPATH`, derived from your Pants source roots. Because Pants gives each
file exactly one source root while Pyrefly makes a file importable under
*every* search path containing it, nested source roots would otherwise give a
module two identities. The plugin removes the nesting structurally: it
re-stages each source root's files into its own non-nesting sibling directory
in the sandbox and passes
[`--disable-search-path-heuristics`](configuration.mdx#disable-search-path-heuristics),
then maps the synthetic paths back to real repo paths in diagnostics, baseline
files, and `suppress` edits — so it is invisible in the output. The diagnostic
goals (`pyrefly-coverage`, `pyrefly-dump-config`, `pyrefly-lsp-config`) skip
the staging and pass your real roots, deduplicated to each file's nearest
root; `[pyrefly].exclude_source_roots` force-drops a root on that path only,
and has no effect on `check`.
- **Third-party dependencies** are materialized into a venv, and Pyrefly's
`--python-interpreter-path` points at it, so Pyrefly discovers `site-packages`
and the target Python version exactly as `import` would at runtime.
When imports or the interpreter resolve differently than you expect, dump the
configuration Pants actually assembles:
```shell
pants pyrefly-dump-config :: # whole repo
pants pyrefly-dump-config src/project:: # a subtree
```
This runs Pyrefly's `dump-config` over the same partitions and file list as
`check`, so it is the right tool for confirming which config file, interpreter,
and source roots a given target resolves against. Two differences from `check`
worth knowing when you read the output: `dump-config` does not re-stage sources,
so the `search-path` it prints is your real source roots (nearest-root deduped)
rather than the synthetic sandbox roots `check` passes; and the reporting
options — `output_format`, `min_severity`, `only`, `baseline`, and
`[pyrefly].args` — apply to `check` only, so they do not appear here.
Common causes of resolution problems:
- **A first-party module reports a missing import** — it is not reachable from a
source root. `pants roots` lists them.
- **A third-party package reports a missing import** — the requirement is not in
that target's resolve, or it ships no types. Add it as a dependency; add
stub-only packages through `[pyrefly].extra_type_stubs`.
- **A whole partition fails to find an interpreter** — no installed interpreter
matches that partition's `interpreter_constraints`.
For implementation details and the plugin's compatibility promise, see the
[`pants-pyrefly` README](https://github.com/tague/pants-pyrefly).
## Feedback
File Pants-integration issues — plugin behavior, goals, search paths, version
pinning — in
[tague/pants-pyrefly/issues](https://github.com/tague/pants-pyrefly/issues), and
Pyrefly type-checking issues in
[facebook/pyrefly/issues](https://github.com/facebook/pyrefly/issues).
---
---
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`](./migrate/index.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/)
## Can I run Pyrefly in the browser / WebAssembly?
Yes, in the [sandbox](https://pyrefly.org/sandbox/). Pyrefly compiles to
WebAssembly for that playground. The WASM crate is **not** a stable embeddable
API yet. See [WebAssembly](./wasm.mdx) for how to build it and what `State`
currently exposes.
## 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: Pytest Support
description: Pyrefly support for pytest fixtures and test navigation.
---
{/*
* 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.
*/}
# Pytest Support
Pyrefly includes built-in support for [pytest](https://docs.pytest.org/), the
popular Python testing framework. This support focuses on fixtures and IDE
workflows for navigating, annotating, and running tests.
> **Note:** Pyrefly's pytest support is automatic when a file imports `pytest`.
> Some IDE features also depend on selecting a Python interpreter where pytest is
> installed.
### Feedback
We welcome your feedback and suggestions. Please share your thoughts and ideas
by opening an issue on [GitHub](https://github.com/facebook/pyrefly/issues).
---
## What is pytest?
pytest is a Python testing framework where tests are usually functions or
methods named `test_*`. Its fixture system lets tests request reusable setup
objects by naming fixture parameters.
```python
import pytest
@pytest.fixture
def username() -> str:
return "Ada"
def test_user(username):
assert username == "Ada"
```
In this example, pytest injects the `username` fixture into the test function
based on the parameter name.
---
## How Pyrefly Supports pytest
Pyrefly recognizes pytest imports and fixture decorators without requiring a
plugin or manual configuration. It:
- Recognizes fixtures declared with `@pytest.fixture`, `@pytest.fixture(...)`,
and imported fixture aliases such as `from pytest import fixture`.
- Resolves fixture parameters in tests and fixtures to their fixture
definitions.
- Finds references from a fixture definition to the test and fixture parameters
that request it.
- Shows optional inlay hints for pytest fixture parameter types.
- Provides quick fixes that add inferred return annotations to fixtures.
- Provides quick fixes that add inferred parameter annotations to tests that use
fixtures.
- Adds runnable CodeLens actions for pytest tests in supported editors.
---
## Fixture Navigation
Pyrefly understands pytest's fixture-by-name convention, so IDE navigation works
across fixture definitions and fixture parameters in the same module.
```python
import pytest
@pytest.fixture
def database_url() -> str:
return "sqlite://"
def test_connection(database_url):
assert database_url.startswith("sqlite")
```
Go-to-definition on the `database_url` parameter in `test_connection` jumps to
the fixture definition. Find-references on the fixture name includes parameters
that request that fixture.
Pyrefly also handles fixture aliases:
```python
from pytest import fixture as reusable
@reusable
def user_id() -> int:
return 42
def test_lookup(user_id):
assert user_id > 0
```
---
## Fixture Type Hints and Quick Fixes
Pyrefly can infer fixture return types and offer quick fixes that make those
types explicit.
```python
import pytest
@pytest.fixture
def enabled():
return True
def test_feature(enabled):
assert enabled
```
In the editor, these quick fixes appear as code actions:
- Place the cursor inside an unannotated fixture to use
`Add pytest fixture type annotation`, which updates the fixture to
`def enabled() -> bool:`.
- Place the cursor on an unannotated test parameter that matches a known fixture
to use `Add pytest fixture parameter type annotation`, which updates the test
to `def test_feature(enabled: bool):`.
- When more than one matching annotation is available, Pyrefly can also offer
batch code actions that annotate all eligible fixtures or fixture parameters
in the file.
---
## Running pytest Tests from the Editor
In supported editors, Pyrefly shows runnable CodeLens actions above pytest tests.
For example, a test method can be run using a pytest node id such as:
```bash
python -m pytest path/to/test_file.py::TestClass::test_method
```
This feature uses the Python interpreter selected for the workspace. If Pyrefly
cannot import pytest from that interpreter, select the correct interpreter or
install pytest in the active environment.
---
## How to Use
You do not need to enable a Pyrefly plugin or add Pyrefly-specific configuration
for pytest support.
1. Install `pytest` in the Python environment used by your editor.
1. Install `pyrefly`.
1. Write pytest fixtures and tests as usual.
1. Run Pyrefly or use the Pyrefly language server in your editor.
---
---
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.
## 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
```
It reports every offending symbol in the same style as `pyrefly check`, as `coverage-missing`
(no annotations at all) or `coverage-partial` (only some), followed by a one-line summary:
```
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
```
When coverage meets the threshold, the findings are printed but the summary is INFO and the exit
code is 0:
```
INFO type coverage 66.67% (4 of 6 typable)
```
Findings go to stdout, the summary to stderr. With `--strict`, annotations that resolve to `Any`
count as untyped. `--output-format` controls the findings 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 Pyrefly's 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.
## Where the skill lives
The skill is a directory in the Pyrefly source tree:
```
tensor-shapes/skills/add-shape-types-to-torch-model/
```
You can either copy the skill into your configuration and use a
slash command like `/add-shape-types-to-torch-model` to invoke it, or you
can just point your agent at the skill file and ask it to follow the steps.
## What should the agent do?
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 `Int` 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 add-shape-types-to-torch-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 `Int`
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 Int, 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 Int operators, type-level arithmetic in shapes, and concatenation.
```
## `Int[X]`
`Int[X]` is a type constructor that bridges runtime integer values to
type-level symbols. It is defined in the `shape_extensions` package,
alongside the other names used throughout this page: `IntVar` (the bound
for shape type parameters), `IntTuple` (the bound for whole-shape type
parameters), and `Elements` (splices a whole-shape type parameter into a
`Tensor` argument list).
### Basics
`Int[X]` denotes the type of an integer value whose type-level identity is
`X`. For example:
- `Int[5]` is the type of the literal `5`
- `Int[N]` (where `N` is a type variable) is the type of an integer whose
value is bound to `N` at the type level
`Int` is a subtype of `int`, so `Int` values can be used anywhere `int` is
expected. However, the reverse is not true — passing a plain `int` where
`Int[X]` is expected loses tracking.
### Arithmetic
Arithmetic on `Int` values produces `Int` results with the corresponding
type-level expression:
| Expression | Type |
|-----------|------|
| `a + b` where `a: Int[A]`, `b: Int[B]` | `Int[A + B]` |
| `a - b` | `Int[A - B]` |
| `a * b` | `Int[A * B]` |
| `a // b` | `Int[A // B]` |
| `a ** b` | `Int[A ** B]` |
**Caution:** `int * Int` produces `Unknown` because the `int` side has no
type-level identity. Use `Int * Int` or literal `* Int` instead.
### `Int[X] | None`
For optional dimensions — parameters that may or may not be present — use
`Int[X] | None`. In the forward method, narrow with
`if param is not None:` to recover `Int[X]` inside the branch:
```python
class Attention[D: IntVar, RK: IntVar](nn.Module):
def __init__(self, dim: Int[D], rank_k: Int[RK] | None = None):
...
def forward[B: IntVar, T: IntVar](self, x: Tensor[[B, T, D]]) -> Tensor[[B, T, D]]:
if self.rank_k is not None:
# rank_k is Int[RK] here
...
```
### Usage patterns
| Pattern | Purpose |
|---------|---------|
| `def __init__(self, dim: Int[D])` | Accept a dimension as a constructor parameter |
| `class Model[D: IntVar](nn.Module)` | Make a dimension a class-level type parameter |
| `def forward[B: IntVar](self, x: Tensor[[B, D]])` | Bind a per-call dimension |
| `self.head_dim = dim // n_head` | Compute a derived dimension (`Int[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[[*Elements[Bs], D]]` | Variadic: any number of leading batch dimensions (`Bs: IntTuple`) |
| `Tensor` (bare) | Shape unknown — tracking gap |
### Variadic dimensions with `Elements`
Use a type parameter bound to `IntTuple`, spliced into the shape list with
`*Elements[...]`, for dimensions that should be propagated without being
enumerated:
```python
def forward[Bs: IntTuple](self, x: Tensor[[*Elements[Bs], InDim]]) -> Tensor[[*Elements[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 Int `D`, use `Tensor[[*Elements[Bs], D]]` not folding `D` into
the variadic carrier itself.
### `.shape` and `.size()`
When `x: Tensor[[B, C, H, W]]`:
- `x.shape` has type `tuple[Int[B], Int[C], Int[H], Int[W]]`
- `x.size(0)` has type `Int[B]`
- `x.size()` has type `tuple[Int[B], Int[C], Int[H], Int[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.
## Installation
Tensor shape checking requires shape-aware stubs to be available through normal
import resolution. Install them from PyPI:
```bash
pip install pyrefly-torch-stubs
```
`pyrefly-torch-stubs` is a PEP 561 stub-only package: it carries type
information for PyTorch, and leaves the runtime `torch` package alone. PyTorch's
own type stubs don't carry shape information, so these stubs take precedence
over them and provide 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).
Installing it also pulls in `pyrefly-shape-extensions`, which provides the
`shape_extensions` package. `shape_extensions` exports `Int` — the bridge
between runtime integer values and type-level symbols. Both packages are
versioned in lockstep with Pyrefly.
Tensor shape support is enabled automatically when Pyrefly can resolve the
`shape_extensions` package, so nothing else needs configuring, as long as
Pyrefly resolves imports against the
[Python environment](../configuration#python-interpreter-path) you installed
into.
### Checking against a local copy of the stubs
The stubs also live in Pyrefly's source tree under
[`tensor-shapes/`](https://github.com/facebook/pyrefly/tree/main/tensor-shapes),
which is where to work if you want to read or modify them. Copy that directory
into your project and point `search-path` at the two package directories inside
it. The paths are relative to the location of your `pyrefly.toml` (or of your
`pyproject.toml`, if you configure Pyrefly under `[tool.pyrefly]`):
```toml
search-path = [
"tensor-shapes/pyrefly-torch-stubs",
"tensor-shapes/pyrefly-shape-extensions",
]
```
A copy that lives inside your project is also checked as project code. The stubs
use PEP 696 type parameter defaults (`class Conv1d[..., S: IntVar = 1]`), which
Pyrefly parses only under `python-version` 3.13 or later, so checking them under
an earlier version reports:
```
ERROR Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) [invalid-syntax]
```
Either raise the version Pyrefly checks against. `python-version` sets the
version your code is checked for; it does not change the interpreter your code
runs on:
```toml
python-version = "3.13"
```
Or, if your project targets an earlier version and you don't want to change how
your own code is checked, exclude the copy from checking instead. Imports still
resolve through `search-path`:
```toml
project-excludes = ["tensor-shapes/**"]
```
## 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 Int
```
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 `Int` 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 Int
```
### Option 2: `shape_extensions.IntVar` (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 `IntVar` that
supports arithmetic (`N + 1` returns `self` instead of raising `TypeError`).
Use old-style generics with `shape_extensions.IntVar`:
```python
from typing import assert_type
import torch
import torch.nn as nn
from torch import Tensor
from shape_extensions import Int, IntVar
N = IntVar("N")
M = IntVar("M")
class Linear(nn.Module):
def __init__(self, n: Int[N], m: Int[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 Int 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 Int, IntVar
class TwoLayerNet[InDim: IntVar, HidDim: IntVar, OutDim: IntVar](nn.Module):
def __init__(
self,
in_dim: Int[InDim],
hid_dim: Int[HidDim],
out_dim: Int[OutDim],
):
super().__init__()
self.fc1 = nn.Linear(in_dim, hid_dim)
self.fc2 = nn.Linear(hid_dim, out_dim)
def forward[B: IntVar](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: IntVar, BlockSize: IntVar, NEmbedding: IntVar, NHead: IntVar, NLayer: IntVar]:
block_size: Int[BlockSize]
vocab_size: Int[VocabSize]
n_layer: Int[NLayer]
n_head: Int[NHead]
n_embd: Int[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: IntVar](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: IntVar, T: IntVar](
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: IntVar](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: IntVar, OutC: IntVar](nn.Module):
def __init__(self, in_c: Int[InC], out_c: Int[OutC]) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(in_c, 128, ...), nn.Conv2d(128, out_c, ...)
)
def forward[B: IntVar, H: IntVar, W: IntVar](
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 Int[128]
```
Fix: add an explicit `Int` field to the config:
```python
@dataclass
class Config[K: IntVar, MlpOut: IntVar]:
num_output_features: Int[K]
mlp_output_dim: Int[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: IntVar, OutDim: IntVar](nn.Module):
def __init__(self, in_dim: Int[InDim], out_dim: Int[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: IntVar](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 `Int` 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 `Int`
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: IntVar, NClasses: IntVar](nn.Module):
def _encode[B: IntVar, C: IntVar, H: IntVar, W: IntVar](
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: IntVar, C: IntVar, H: IntVar, W: IntVar](
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: IntVar, B: IntVar, C: IntVar, H: IntVar, W: IntVar](
self, x: Tensor[[B, C, H, W]], depth: Int[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: IntVar, C: IntVar, H: IntVar, W: IntVar](
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: IntVar, C: IntVar, H: IntVar, W: IntVar](
self, x: Tensor[[B, C, H, W]], depth: Int[1]
) -> Tensor[[B, C // 2, H * 2, W * 2]]: ...
@overload
def _chain[I: IntVar, B: IntVar, C: IntVar, H: IntVar, W: IntVar](
self, x: Tensor[[B, C, H, W]], depth: Int[I]
) -> Tensor[[B, C // 2 ** I, H * 2 ** I, W * 2 ** I]]: ...
def _chain[I: IntVar, B: IntVar, C: IntVar, H: IntVar, W: IntVar](
self, x: Tensor[[B, C, H, W]], depth: Int[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: Int[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: IntVar](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 `Int`, `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 `Int`, not `int`.
(`Int[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 `Int[...]` and add class-level type
parameters:
```python
class BaselineActor[S: IntVar, A: IntVar](nn.Module):
def __init__(self, state_size: Int[S], action_size: Int[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: IntVar](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: IntVar](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 Int, IntVar
class BaselineActor[S: IntVar, A: IntVar](nn.Module):
def __init__(self, state_size: Int[S], action_size: Int[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: IntVar](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_extensionsintvar-runtime-compatible)
for the `shape_extensions.IntVar` import style, which requires old-style
generics.
:::
## Key concepts
- **`Int[X]`** bridges runtime integer values to type-level symbols. Constructor
parameters that determine tensor dimensions should be `Int[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: IntVar, DK: IntVar, DInner: IntVar](nn.Module):
def __init__(
self,
n_head: Int[NHead],
d_k: Int[DK],
d_inner: Int[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: IntVar, T: IntVar](
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 `Int` — it's an iteration count that
doesn't flow into any tensor dimension. Only values that determine tensor
shapes need to be `Int`.
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]` for a whole-shape type parameter `S: IntTuple`, 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: IntVar](nn.Module):
def __init__(self, c: Int[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: IntVar, NHead: IntVar](nn.Module):
def __init__(
self,
n_embd: Int[NEmbedding],
n_head: Int[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: IntVar, T: IntVar](
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, `Int` 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
:::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]`
accepts a tensor with any number of leading dimensions followed by a `3`,
and returns one with the same leading dimensions followed by a `4` — see
[Variadic dimensions with `Elements`](./tensor-shapes-reference.mdx#variadic-dimensions-with-elements)
in the Reference page for the syntax that expresses this.
**`Int[X]`** bridges runtime integer values to the type level. When
`x: Tensor[[3, 4]]`, then `x.shape` has type `tuple[Int[3], Int[4]]` — you
can extract dimensions from tensors and use them to construct new ones.
Arithmetic works too: if `a: Int[3]` and `b: Int[4]`, then
`a * b: Int[12]`.
**Generic type parameters** let you write shape-polymorphic modules:
```python
class Linear[N: IntVar, M: IntVar]:
def __init__(self, n: Int[N], m: Int[M]):
...
def forward[Xs: IntTuple](self, inp: Tensor[[*Elements[Xs], N]]) -> Tensor[[*Elements[Xs], M]]:
...
linear: Linear[3, 4] = Linear(3, 4)
inp: Tensor[[2, 5, 3]] = ...
x: Tensor[[2, 5, 4]] = linear(inp)
```
`Int` 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: IntVar, B: IntVar](a: Int[A], b: Int[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: IntVar, K: IntVar, N: IntVar](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
@type_shape_dsl_function
def repeat_shape(shape: IntTuple, repeats: IntTuple) -> IntTuple:
if len(repeats) < len(shape):
return dsl.Invalid(
"Number of dimensions of repeat dims can not be smaller than number of dimensions of tensor"
)
extra = len(repeats) - len(shape)
return dsl.IntTuple(
(
repeats[index] if index < extra else shape[index - extra] * repeats[index]
for index in range(len(repeats))
)
)
```
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.
---
---
title: WebAssembly
slug: /wasm
description: How Pyrefly's WebAssembly build is used, built, and what the current API covers
---
{/*
* 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.
*/}
# WebAssembly
Pyrefly can compile to WebAssembly so a browser can run the type checker and a
subset of language-server features without a native `pyrefly` binary. That is
how the [Pyrefly sandbox](https://pyrefly.org/sandbox/) works.
:::warning Not a stable embeddable API
The WASM crate primarily powers the sandbox. Maintainers have not settled a
complete, backwards-compatible API for embedding Pyrefly in other web apps.
Method names and payloads can change. Test changes made for the sandbox in the
sandbox itself; treat `pyrefly_wasm` as an internal interface if you build from
source.
:::
## Build from source
The crate lives in
[`pyrefly_wasm/`](https://github.com/facebook/pyrefly/tree/main/pyrefly_wasm).
The website wires it into the sandbox under `website/src/sandbox/`.
### Prerequisites
- Linux or macOS (website WASM workflows are not supported on native Windows;
use WSL).
- [Rust](https://rustup.rs/) with `wasm32-unknown-unknown`.
- [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer/) and
[`wasm-opt`](https://github.com/WebAssembly/binaryen) (`wasm-opt` must be on
`PATH`; `wasm-pack`’s bundled optimizer is disabled because it is outdated).
- Clang / LLVM. On macOS: `brew install llvm`. On Fedora/CentOS:
`sudo dnf install clang`. If zstd fails to compile on a Mac, put Homebrew
LLVM first on `PATH` (`llvm-config --version` should resolve).
- Node.js and Yarn, if you will run the website.
### Build the crate
From the repository root:
```bash
cd pyrefly_wasm
./build.sh
```
That runs `wasm-pack` for the `web` target and then `wasm-opt -Os`. Pass
`--BUILD_FOR_TEST` to target `nodejs` (used by website Jest tests).
Output lands in `pyrefly_wasm/target/` (`pyrefly_wasm.js` and
`pyrefly_wasm_bg.wasm`). Copy those into the sandbox (or test) paths the
website README describes when you need a local rebuild.
### Test changes in the sandbox
See [`website/README.md`](https://github.com/facebook/pyrefly/blob/main/website/README.md).
In short:
```bash
cd website
yarn install-with-wasm-deps # first time: build WASM + yarn install
yarn start-with-wasm # rebuild WASM and start Docusaurus
```
`yarn start` / `yarn install` only work on the static docs and do not rebuild
WASM. The sandbox is the primary consumer of `pyrefly_wasm`, so exercise the
affected sandbox behavior after changing its implementation or API.
## Current API
`pyrefly_wasm` exports a `State` class (`new State(version)`) wrapping the
in-tree playground. The exports in
[`pyrefly_wasm/lib.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly_wasm/lib.rs)
are the authoritative API reference. The current JavaScript names are:
| Method | Role |
| --- | --- |
| `updateSandboxFiles(files, forceUpdate)` | Replace the virtual project (`Record` of path → source). Returns an optional error string. |
| `updateSingleFile(filename, content)` | Update one file. |
| `setActiveFile(filename)` | Choose which file later queries apply to. |
| `getErrors()` | Type errors for the project. |
| `hover(line, column)` | Hover payload at a 0-based position, or `null`. |
| `gotoDefinition(line, column)` | Definition ranges, or `null`. |
| `autoComplete(line, column)` | Completions. |
| `inlayHint(callArgumentNames)` | Inlay hints. |
| `semanticTokens(range)` | Semantic tokens for an optional range. |
| `semanticTokensLegend()` | Token types/modifiers legend. |
Positions match the playground (`line` / `column` as `i32`). Payloads are
serde-serialized into JS; there is no TypeScript `.d.ts` from `wasm-pack`
(`--no-typescript`). The sandbox keeps a local
[`PyreflyState`](https://github.com/facebook/pyrefly/blob/main/website/src/sandbox/Sandbox.tsx)
interface.
Some native features are compiled out with
`#[cfg(not(target_arch = "wasm32"))]`. If a WASM build fails with `could not
find … in the crate root`, that crate is likely excluded from the WASM
target. See [`pyrefly_wasm/README.md`](https://github.com/facebook/pyrefly/blob/main/pyrefly_wasm/README.md).
## Configuration
Sandbox files are an in-memory project, not your disk workspace. There is no
separate “WASM config” documented beyond what the playground/`State` accepts
when creating the instance (`version` is the Python version string passed to
`State`). For CLI and IDE configuration, use the
[configuration reference](./configuration.mdx).
## Related source
- [`pyrefly_wasm/README.md`](https://github.com/facebook/pyrefly/blob/main/pyrefly_wasm/README.md) — crate build notes
- [`website/README.md`](https://github.com/facebook/pyrefly/blob/main/website/README.md) — sandbox and docs site
- [Sandbox](/sandbox)