Pyrefly is a fast, powerful static type checker built to work seamlessly with Python. It combines high-speed type analysis with rich IDE integration, helping you write better code through instant feedback and comprehensive error detection.
This guide walks you through setting up and using Pyrefly in your Python projects.
Prerequisites
Before getting started with Pyrefly, ensure you have:
- A recent version of Python installed (Python 3.9 or higher is recommended).
- Familiarity with Python type annotations.
Step 1 — Installing Pyrefly
In this section, you'll install Pyrefly and verify the installation works correctly.
Pyrefly releases new versions every Monday on PyPI, with additional releases throughout the week for new features and fixes. You can install it using several Python package managers.
For this guide, you'll use pip, but uv, poetry, pixi, and conda work equally well.
Before installing Pyrefly, create a dedicated directory for your project and move into it:
mkdir my-pyrefly-project
cd my-pyrefly-project
All remaining commands in this guide will be run from inside this directory. Before installing Pyrefly, create and activate a virtual environment:
python3 -m venv .venv
source .venv/bin/activate
Now, install Pyrefly with pip:
pip3 install pyrefly
After installation completes, confirm Pyrefly is available by checking its version:
pyrefly --version
A successful installation displays the version number:
pyrefly 0.43.1
With Pyrefly installed, you can move on to initializing your project configuration.
Step 2 — Initializing your project
Now that Pyrefly is ready, you'll set up a basic project configuration. Pyrefly provides an initialization command that creates the configuration file automatically.
Navigate to your project directory and run:
pyrefly init
INFO New config written to `/path/to/my-pyrefly-project/pyrefly.toml`
INFO Running pyrefly check...
INFO Checking project configured at `/path/to/my-pyrefly-project/pyrefly.toml`
ERROR Failed to run pyrefly check: No Python files matched patterns `/path/to/my-pyrefly-project/**/*.py*`, `/path/to/my-pyrefly-project/**/*.ipynb`
This command either updates your existing pyproject.toml with Pyrefly settings or creates a new pyrefly.toml file in your project directory. If you're migrating from another type checker like Mypy or Pyright, pyrefly init attempts to convert your existing configuration.
Pyrefly also runs an initial pyrefly check as part of pyrefly init. If your project doesn't contain any Python files yet, you'll see an error like the one above—this is expected and will go away once you add Python or notebook files that match the configured patterns.
The initialization creates a minimal working configuration. By default, your pyrefly.toml might look like this:
[tool.pyrefly]
project-includes = [
"**/*.py*",
"**/*.ipynb",
]
The project-includes setting tells Pyrefly which files to analyze:
**/*.py*matches all Python files (including.pyand.pyi) anywhere in your project.**/*.ipynbmatches all Jupyter notebooks.
These patterns are recursive and are evaluated relative to your project root. If you prefer to limit analysis to a specific directory like src, you can update the config to:
[tool.pyrefly]
project-includes = [
"src/**/*.py*",
"src/**/*.ipynb",
]
With configuration in place, you can start type checking your code.
Step 3 — Running your first type check
In this step, you'll create a simple Python file with intentional type errors, then use Pyrefly to detect them.
Create a directory named src and add a file called main.py:
def calculate_total(price: float, quantity: int) -> float:
return price * quantity
total = calculate_total(19.99, 3)
print(f"Total: ${total}")
# This will trigger a type error
wrong_total = calculate_total("10.50", 5)
The function calculate_total expects a float and an integer. Passing a string as the first argument breaks the type contract.
Run Pyrefly on your source directory:
pyrefly check
Pyrefly analyzes your code and reports the type mismatch:
ERROR Argument `Literal['10.50']` is not assignable to parameter `price` with type `float` in function `calculate_total` [bad-argument-type]
--> src/main.py:9:31
|
9 | wrong_total = calculate_total("10.50", 5)
| ^^^^^^^
|
INFO 1 error
The error message pinpoints exactly where the problem occurs and explains what went wrong. This immediate feedback prevents bugs from reaching production.
To see a summary of error types in your project, add the --summarize-errors flag:
pyrefly check --summarize-errors
This displays both individual errors and statistics about error categories across your codebase.
Next, you'll learn how to configure Pyrefly's behavior more precisely.
Step 4 — Configuring Pyrefly's type checking
In this step, you'll explore how Pyrefly catches potential runtime errors through its type analysis.
Update your main.py to demonstrate Pyrefly's capabilities:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
def process_user(user_id: int) -> str:
username = find_user(user_id)
return username.upper()
The function find_user returns Optional[str], meaning it can return either a string or None. Calling upper() directly on this result without checking for None creates a potential crash.
Run Pyrefly:
pyrefly check
Pyrefly identifies the issue:
ERROR Object of class `NoneType` has no attribute `upper` [missing-attribute]
--> src/main.py:11:12
|
11 | return username.upper()
| ^^^^^^^^^^^^^^
|
INFO 1 error
The error catches a potential runtime failure—calling upper() on None would crash your application if find_user returns None.
Fix the issue by adding a null check:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
def process_user(user_id: int) -> str:
username = find_user(user_id)
if username is not None:
return username.upper()
return "Unknown"
Run Pyrefly again:
pyrefly check
INFO Checking project configured at `/path/to/my-pyrefly-project/pyrefly.toml`
INFO No errors
With proper null checking, your code passes Pyrefly's verification. This pattern of handling Optional types correctly prevents runtime errors before they happen. You can explore additional Pyrefly configuration options to customize the checker's behavior for your project.
Step 5 — Working with advanced type features
After establishing basic type checking, you can leverage Python's more sophisticated type system features. Pyrefly handles generic types, protocols, and type narrowing efficiently.
Using generics for reusable code
Generics let you write functions and classes that work with multiple types while maintaining type safety. Update your main.py to include a generic container:
from typing import Generic, TypeVar
T = TypeVar('T')
class Storage(Generic[T]):
def __init__(self, initial_value: T) -> None:
self._value = initial_value
def get_value(self) -> T:
return self._value
def set_value(self, new_value: T) -> None:
self._value = new_value
int_storage = Storage(42)
str_storage = Storage("Hello")
print(int_storage.get_value() + 10)
print(str_storage.get_value().upper())
# This will cause a type error
int_storage.set_value("Not a number")
Run Pyrefly:
pyrefly check
ERROR Argument `Literal['Not a number']` is not assignable to parameter `new_value` with type `int` in function `Storage.set_value` [bad-argument-type]
--> src/main.py:24:23
|
24 | int_storage.set_value("Not a number")
| ^^^^^^^^^^^^^^
|
Pyrefly tracks the generic type parameter throughout your code. When you create Storage(42), it knows this instance works with integers, so passing a string to set_value triggers an error.
Working with protocols for structural typing
Protocols define interfaces without requiring explicit inheritance. They're useful when you want duck typing with type safety.
Add a protocol to your file:
from typing import Protocol
class Renderable(Protocol):
def render(self) -> str:
...
class Button:
def render(self) -> str:
return "<button>Click me</button>"
class Label:
def render(self) -> str:
return "<label>Text field</label>"
class BrokenWidget:
def display(self) -> str:
return "I don't implement render()"
def show_component(component: Renderable) -> None:
print(component.render())
show_component(Button())
show_component(Label())
show_component(BrokenWidget())
Running Pyrefly reveals the protocol violation:
pyrefly check
ERROR Argument `BrokenWidget` is not assignable to parameter `component` with type `Renderable` in function `show_component` [bad-argument-type]
--> src/main.py:29:16
|
29 | show_component(BrokenWidget())
| ^^^^^^^^^^^^^^
|
Protocol `Renderable` requires attribute `render`
INFO 1 error
(.venv) stanley@MACOOKs-MacBook-Pro
BrokenWidget lacks the render() method required by the Renderable protocol. Pyrefly catches this mismatch even though there's no explicit inheritance relationship.
These advanced features give you flexibility without sacrificing type safety. As your application grows, Pyrefly scales with you, maintaining fast checking times even on large codebases.
When introducing Pyrefly to an existing project, you might encounter hundreds or thousands of type errors. Fixing them all immediately isn't always practical.
Pyrefly provides a way to mark existing errors as known issues, giving you a clean starting point. You can then address them gradually.
Create a file with several type issues:
def old_function(data):
return data.upper()
def another_function(x, y):
return x + y
result1 = old_function(123)
result2 = another_function("text", 456)
Running Pyrefly shows multiple errors:
pyrefly check
INFO Checking project configured at `/path/to/my-pyrefly-project/pyrefly.toml`
ERROR Function `old_function` is missing a return type annotation [missing-return-annotation]
--> src/legacy.py:1:1
|
1 | def old_function(data):
| ^^^^^^^^^^^^^^^^^^^^^^^
|
ERROR Function `another_function` is missing a return type annotation [missing-return-annotation]
--> src/legacy.py:4:1
|
4 | def another_function(x, y):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
ERROR Object of class `int` has no attribute `upper` [missing-attribute]
--> src/legacy.py:7:23
|
7 | result1 = old_function(123)
| ^^^^^^^^^^^^^^^^^
|
INFO 3 errors
Rather than fixing everything immediately, suppress these errors:
pyrefly check --suppress-errors
INFO Checking project configured at `/path/to/my-pyrefly-project/pyrefly.toml`
INFO Suppressed 3 errors
Pyrefly adds # pyrefly: ignore comments next to each error:
def old_function(data): # pyrefly: ignore
return data.upper() # pyrefly: ignore
def another_function(x, y): # pyrefly: ignore
return x + y
result1 = old_function(123)
result2 = another_function("text", 456)
After suppression, running Pyrefly again shows a clean result:
pyrefly check
INFO Checking project configured at `/path/to/my-pyrefly-project/pyrefly.toml`
INFO No errors
New type errors will still be caught, but existing issues are temporarily silenced. As you refactor code, remove the ignore comments and add proper type annotations.
If your code formatter moves or duplicates the ignore comments, clean them up:
pyrefly check --remove-unused-ignores
This removes any ignore comments that no longer correspond to actual errors, keeping your codebase clean.
Final thoughts
By this point, you’ve seen how to install Pyrefly, initialize a project, and run your first checks against both simple examples and more realistic code. You’ve configured which files Pyrefly should analyze, handled common issues like Optional values, and explored more advanced types such as generics and protocols. You’ve also learned how to gradually introduce Pyrefly into existing code by suppressing and later cleaning up legacy errors.
From here, you can start applying Pyrefly to your real projects: tighten your configuration, integrate it into your editor and CI, and incrementally raise the level of type safety in your codebase. As your project grows, Pyrefly can grow with it, giving you fast, actionable feedback that helps keep your Python code reliable and easier to maintain over time.