Back to Scaling Python Applications guides

Getting Started with Pyrefly

Stanley Ulili
Updated on November 28, 2025

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:

Output
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
Output
 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:

pyrefly.toml
[tool.pyrefly]
project-includes = [
    "**/*.py*",
    "**/*.ipynb",
]

The project-includes setting tells Pyrefly which files to analyze:

  • **/*.py* matches all Python files (including .py and .pyi) anywhere in your project.
  • **/*.ipynb matches 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:

src/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:

Output
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:

src/main.py
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:

Output
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:

src/main.py
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
Output
 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:

src/main.py
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
Output
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:

src/main.py
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
Output
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:

src/legacy.py
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
Output
 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
Output
 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:

src/legacy.py
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
Output
 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.

Got an article suggestion? Let us know
Next article
Get Started with Job Scheduling in Python
Learn how to create and monitor Python scheduled tasks in a production environment
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.