# Getting Started with Pyrefly

[Pyrefly](https://pyrefly.org/) 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.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/O7zP2XCs0do" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## Prerequisites

Before getting started with Pyrefly, ensure you have:

- A recent version of [Python](https://www.python.org/downloads/) 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:

```command
mkdir my-pyrefly-project
```

```command
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:

```command
python3 -m venv .venv
```

```command
source .venv/bin/activate
```
Now, install Pyrefly with `pip`:

```command
pip3 install pyrefly
```

After installation completes, confirm Pyrefly is available by checking its version:

```command
pyrefly --version
```


A successful installation displays the version number:

```text
[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:

```command
pyrefly init
```

```text
[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:

```toml
[label 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:

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

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

```python
[label 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:

```command
pyrefly check
```

Pyrefly analyzes your code and reports the type mismatch:

```text
[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:

```command
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:

```python
[label 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:

```command
pyrefly check
```

Pyrefly identifies the issue:

```text
[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:

```python
[label 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)
[highlight]
    if username is not None:
        return username.upper()
    return "Unknown"
[/highlight]
```

Run Pyrefly again:

```command
pyrefly check
```

```text
[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](https://pyrefly.org/en/docs/configuration/) 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:

```python
[label 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:

```command
pyrefly check
```

```text
[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:

```python
[label 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:

```command
pyrefly check
```

```text
[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:

```python
[label 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:

```command
pyrefly check
```

```text
[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:

```command
pyrefly check --suppress-errors
```

```text
[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:

```python
[label 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:

```command
pyrefly check
```

```text
[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:

```command
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.
