pyrefly
Pyrefly is a fast Python type checker and language server built by Meta, written in Rust. It provides lightning-fast type checking with full IDE features: code completion, go-to definition, hover types, inlay hints, semantic highlighting, and more. Available on PyPI and as extensions for VSCode, Neovim, JetBrains, Emacs, Helix, and others.
Key facts:
- Faster than mypy and pyright — module-level incrementality + parallel checking leverages Rust performance
- Type inference for variables and return types (not just explicit annotations)
- Flow types — refines static types based on control flow (e.g., after
if x is Nonecheck,xis narrowed to non-None) - LSP built in via
pyrefly lsp— works with any LSP-capable editor - Weekly releases (Mondays) on PyPI; actively developed by Meta
Docs: https://pyrefly.org/en/docs/
Installation
pip install pyrefly # pip
uv add --dev pyrefly # uv (recommended, adds as dev dependency)
poetry add --group dev pyrefly # poetry
conda install pyrefly # conda
uvx pyrefly check . # zero-install via uvx
Core Commands
pyrefly init # Initialize config; migrates from mypy/pyright automatically
pyrefly check # Type-check the whole project (uses config file)
pyrefly check --summarize-errors # Check with a summary of error categories
pyrefly check src/ tests/ # Check specific files or directories
pyrefly suppress # Add # pyrefly: ignore to all erroring lines
pyrefly suppress --remove-unused # Remove ignore comments that are no longer needed
pyrefly lsp # Start language server (for editor integration)
pyrefly dump-config [file.py] # Debug config discovery and effective settings
Key flags
--config / -c <path> # Explicitly specify config file
--python-version 3.12 # Override Python version for sys.version checks
--python-platform linux # Override sys.platform assumption
--output-format github # Emit GitHub Actions workflow commands for inline PR annotations
--output-format json # JSON output for programmatic consumption
--summarize-errors # Show summary of error categories at the end
--suppress-errors # Add # pyrefly: ignore to all erroring lines (same as pyrefly suppress)
--remove-unused-ignores # Remove stale # pyrefly: ignore comments
--baseline <path> # Use baseline JSON to suppress pre-existing errors
--update-baseline # Regenerate the baseline file
--min-severity warn # Also show warn/info diagnostics (default: error only)
--search-path <dir> # Add import resolution root (like mypy_path / extraPaths)
--project-excludes <glob> # Exclude file patterns
--ignore-missing-imports <module> # Treat specific missing module as Any
--ignore-errors-in-generated-code # Skip files containing '@generated'
--permissive-ignores # Also respect # pyre-ignore, # mypy: ignore, etc.
--check-unannotated-defs # Type-check unannotated function bodies (default: true)
Configuration
Pyrefly reads config from pyrefly.toml (top-level keys) or pyproject.toml ([tool.pyrefly] section). It's found automatically by walking up the directory tree — also detects setup.py, mypy.ini, and pyrightconfig.json as project root markers.
Precedence: CLI flags > config file > Pyrefly defaults
Starter config (pyproject.toml)
[tool.pyrefly]
# Directories/globs to type-check
project-includes = ["src", "tests"]
# Python version for sys.version checks
python-version = "3.12"
# Silence errors in @generated files
ignore-errors-in-generated-code = true
# Per-error-code severity: "error" | "warn" | "ignore" | false (same as ignore)
[tool.pyrefly.errors]
# implicit-any = "warn" # enable to enforce fully-annotated codebase
# deprecated = "warn" # already "warn" by default
Full pyrefly.toml reference
project-includes = ["src/**/*.py", "tests/**/*.py"]
project-excludes = ["src/generated/**"] # Added on top of defaults (node_modules, venv, etc.)
python-version = "3.12"
python-platform = "linux"
# Import resolution roots (like mypy_path / Pyright's extraPaths)
search-path = ["src"]
# Treat these missing modules as Any (only when not found)
ignore-missing-imports = ["boto3", "mypy_boto3_*"]
# Always treat these as Any (even when found)
# replace-imports-with-any = ["legacy_module"]
ignore-errors-in-generated-code = true
# Also respect # pyre-ignore and # mypy: ignore comments
permissive-ignores = false
# Show warn/info diagnostics too (default: "error")
min-severity = "error"
# Output format for pyrefly check
output-format = "full-text" # full-text | min-text | json | github | omit-errors
# Baseline file for suppressing pre-existing errors (experimental)
# baseline = "pyrefly-baseline.json"
# Type-check bodies of unannotated functions (false = mypy's old default)
check-unannotated-defs = true
# Return type inference: "checked" | "annotated" | "never"
infer-return-types = "checked"
# Infer container types from first use (true = mypy-like, false = Pyright-like)
infer-with-first-use = true
[errors]
bad-param-name-override = "warn" # Downgrade from error to warning
implicit-any = "warn" # Off by default; enable for strict codebases
# Per-directory overrides (sub-config)
[[sub-config]]
matches = ["tests/**"]
check-unannotated-defs = false # More lenient in tests
[[sub-config]]
matches = ["src/legacy/**"]
check-unannotated-defs = false
ignore-missing-imports = ["*"]
Config discovery
Pyrefly walks up from the current directory looking for:
pyrefly.toml(parsed for config)pyproject.tomlwith[tool.pyrefly](parsed for config)setup.py,mypy.ini,pyrightconfig.json(used as project root markers only)
Use -c path/to/pyrefly.toml to bypass auto-discovery.
Error Suppression
# Suppress all errors on this line
x: str = 1 # pyrefly: ignore
# Suppress specific error code
x: str = 1 # pyrefly: ignore[bad-assignment]
# Standard type: ignore (also respected)
x: str = 1 # type: ignore
# Suppress ALL errors in an entire file (put near top of file)
# pyrefly: ignore-errors
Bulk suppression workflow (onboarding existing codebases)
# Step 1: Mark all existing errors as ignored
pyrefly suppress
# Step 2: Run your formatter to normalize comment placement
black . && isort .
# Step 3: Remove stale ignores that are no longer needed
pyrefly suppress --remove-unused
# Repeat until clean, then add to CI
Baseline files (experimental)
Baseline files suppress pre-existing errors without adding inline comments — useful for large migrations or version upgrades where you want clean diffs:
# Generate baseline from all current errors
pyrefly check --baseline="pyrefly-baseline.json" --update-baseline
# Subsequent checks only report newly-introduced errors
pyrefly check --baseline="pyrefly-baseline.json"
# pyproject.toml — persist baseline in config
[tool.pyrefly]
baseline = "pyrefly-baseline.json"
Common Error Codes
| Code | Meaning |
|---|---|
bad-assignment |
Value incompatible with variable's type annotation |
bad-argument-type |
Wrong type passed to a function argument |
bad-argument-count |
Too many or too few positional arguments |
missing-argument |
Required argument not provided |
bad-return |
Return value incompatible with declared return type |
bad-override |
Subclass method violates Liskov Substitution Principle |
missing-import |
Module cannot be found |
invalid-annotation |
Incorrect usage of Final, ClassVar, TypeVar, etc. |
annotation-mismatch |
Same variable annotated with different types in different branches |
bad-unpacking |
Unpacking into wrong number of variables |
bad-specialization |
Generic type specialized with wrong number/type arguments |
deprecated |
Usage of a @deprecated class or function (severity: warn) |
implicit-any |
Implicit Any inferred (off by default; enable for strict codebases) |
implicit-import |
Submodule accessed without explicit import (severity: warn) |
reveal-type |
Informational — shows inferred type of an expression |
Full list: https://pyrefly.org/en/docs/error-kinds/
Gradual Typing Adoption
For adding Pyrefly to an existing untyped codebase, start permissive and tighten over time:
pyrefly init— initializes config and auto-migrates from mypy/pyright if presentpyrefly suppress— silences all existing errors with# pyrefly: ignore- Add to CI — any new code must pass type checks (see CI section below)
- Fix suppressed errors gradually, module by module
- Use
sub-configto enforce different strictness in different parts of the codebase
# pyproject.toml — allow lenient checking in legacy dirs
[[tool.pyrefly.sub-config]]
matches = ["src/legacy/**"]
check-unannotated-defs = false
[[tool.pyrefly.sub-config]]
matches = ["src/new_module/**"]
# strict defaults apply here — no overrides needed
CI Integration (GitHub Actions)
# .github/workflows/typecheck.yml
name: Pyrefly Type Check
on:
pull_request:
branches: [main]
workflow_dispatch:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Install dependencies
run: |
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
- name: Install Pyrefly
run: pip install pyrefly
- name: Run Pyrefly
# --output-format=github emits inline PR annotations
run: pyrefly check --output-format=github
You can also drop pyrefly check into an existing workflow that already sets up your Python environment — the pyproject.toml / pyrefly.toml config is detected automatically.
Pre-commit Integration
Using the official facebook/pyrefly-pre-commit hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/facebook/pyrefly-pre-commit
rev: v0.x.x # pin to latest release
hooks:
- id: pyrefly
IDE / Editor Integration
VSCode, Cursor, Windsurf, Kiro
Install the Pyrefly extension from the marketplace.
Useful settings:
{
"python.pyrefly.displayTypeErrors": "force-on",
"python.pyrefly.diagnosticMode": "workspace"
}
Neovim (0.11+)
-- Via Mason (recommended)
require("mason-lspconfig").setup({ ensure_installed = { "pyrefly" } })
vim.lsp.enable({"pyrefly"})
-- Or with custom command (e.g., for uv-installed pyrefly)
vim.lsp.config('pyrefly', { cmd = { 'uvx', 'pyrefly', 'lsp' } })
JetBrains / PyCharm
Settings → Python → Tools → Pyrefly → Enable checkbox
Emacs (eglot, built-in since Emacs 29)
(add-to-list 'eglot-server-programs
'((python-ts-mode python-mode) . ("pyrefly" "lsp")))
Helix
# languages.toml
[language-server.pyrefly]
command = "pyrefly"
args = ["lsp"]
[[language]]
name = "python"
language-servers = ["pyrefly"]
Vim + coc.nvim
"languageserver": {
"pyrefly": {
"command": "pyrefly",
"args": ["lsp"],
"filetypes": ["python"],
"rootPatterns": ["pyrefly.toml", "pyproject.toml", ".git"]
}
}
Comparison with mypy / pyright
vs mypy:
- Pyrefly is significantly faster (Rust + module-level incrementality)
- Pyrefly infers return types by default; mypy traditionally requires explicit annotations
check-unannotated-defs = falsemimics mypy's old default behaviorpermissive-ignores = truemakes Pyrefly respect existing# type: ignorecommentspyrefly initauto-migratesmypy.ini/[tool.mypy]configuration
vs pyright:
- Both are fast; Pyrefly uses Rust, Pyright uses TypeScript
infer-with-first-use = falsemakes container type inference behave like Pyright (list[Any]vs inferring from first use)pyrefly initauto-migratespyrightconfig.json- Both support the LSP; Pyright's LSP is Pylance in VSCode
Type system support: PEP 484, 526, 544 (Protocols), 589 (TypedDict), 604 (X | Y union syntax), 612 (ParamSpec), 673 (Self), 675 (LiteralString), 695 (type aliases), and more.
Type Checking Concepts
Flow types (type narrowing)
x: int | None = get_value()
if x is not None:
print(x + 1) # x is narrowed to int — no error
def f(s: str | None) -> str:
if s is None:
return ""
return s.upper() # s: str here, not str | None
Type inference
def process(items): # items: Any (no annotation = Any)
return len(items) # return type inferred as int
xs = [] # inferred as list[int] from first use
xs.append(1) # ok
xs.append("two") # error: str not assignable to int
Using reveal_type for debugging
from typing import reveal_type
x = [1, 2, 3]
reveal_type(x) # Revealed type: list[int]