← 输入框|realpython.com|刷新
A glowing bee highlights a line on a long printout from a machine with Checking, Inferring, and Coverage gauges, while a person with a clipboard points at it.

Pyrefly: A Fast Static Type Checker for Python Projects

by Cameron Riddell Updated Reading time estimate 17m intermediate tools

Pyrefly is a fast static type checker for Python that helps you catch type-related issues before running your code. It also includes features for generating type hints and measuring type coverage across a codebase.

In this tutorial, you’ll use Pyrefly to catch type errors, add type hints to existing code, track your project’s type coverage, and see how its speed compares. You’ll get the most out of it if you’re comfortable writing functions and adding basic type hints in Python 3.10 or later. The table below gives you a quick comparison with two popular alternatives, Pyright and mypy:

Pyrefly Pyright mypy
Type checking ✅ ✅ ✅
Type-hint generation ✅ — —
Type coverage ✅ — —
Speed Fast Moderate Moderate

The table highlights where the Pyrefly CLI goes beyond other tools: it can generate type hints and measure coverage, and it’s built to be very fast. The coverage comparison needs context: Pyright’s --verifytypes checks a package’s public interface, while mypy’s reporting flags can summarize how much of your code has type hints.

Take the Quiz: Test your knowledge with our interactive “Pyrefly: A Fast Static Type Checker for Python Projects” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Pyrefly: A Fast Static Type Checker for Python Projects

Test your understanding of Pyrefly, a fast Python type checker, from catching type errors to generating type hints and measuring type coverage.

Get Started With Pyrefly

Pyrefly is available on PyPI. To follow along, first create and activate a virtual environment, then install Pyrefly with pip:

Language: Shell
$ mkdir pyrefly-demo
$ cd pyrefly-demo
$ python -m venv .venv
$ source .venv/bin/activate
(.venv) $ python -m pip install pyrefly==1.2.0

Note that on Windows, you’ll need to activate the environment with .venv\Scripts\activate instead.

One of the quickest ways to try Pyrefly is with pyrefly snippet, which type-checks a short piece of Python code without creating a file. The following snippet hints x as a string but assigns it an integer:

Language: Shell
(.venv) $ pyrefly snippet "x: str = 1"
ERROR `Literal[1]` is not assignable to `str` [bad-assignment]
 --> snippet:1:10
  |
1 | x: str = 1
  |    ---   ^
  |    |
  |    declared type
  |
 INFO 1 error

Pyrefly reports an error because the variable x is hinted as str, but the assigned value is an int. The error shows it as Literal[1], the most specific type Pyrefly can infer for 1. Assigning a value of the correct type removes the error:

Language: Shell
(.venv) $ pyrefly snippet 'x: str = "Hello, World!"'
 INFO 0 errors

Like other type-checking tools, Pyrefly checks your code against its type hints without executing it. As your projects grow, this can help you catch type-related issues before they become runtime bugs.

In addition to being a command-line tool for type-checking your code, Pyrefly includes a language server for editor integration. This tutorial focuses on the command-line workflow, but you can find setup instructions for popular editors in the official documentation.

Catch Type Errors in a Python Project Before Execution

While pyrefly snippet is useful for quickly checking a small piece of code, most type checking happens across modules, packages, and even entire projects. You can use Pyrefly to analyze Python files in a codebase with pyrefly check.

First, initialize Pyrefly in your project folder. The --non-interactive flag accepts the default settings instead of prompting you for each one:

Language: Shell
(.venv) $ pyrefly init --non-interactive
 INFO New config written to `.../pyrefly.toml`
 INFO Running pyrefly check...
 ...

The command creates a pyrefly.toml configuration file that instructs Pyrefly to check all Python files and Jupyter notebooks in the current project. You may also notice that pyrefly init automatically runs a type check after creating the configuration file. Since the folder doesn’t contain any Python files yet, this first check has nothing to report, so instead, focus on the configuration file that pyrefly init creates.

Here’s what that file may look like:

Language: TOML Filename: pyrefly.toml
project-includes = [
    "**/*.py*",
    "**/*.ipynb",
]

With the project configured, you can run pyrefly check to analyze your Python files. Say that you have the following code file:

Language: Python Filename: incorrect_types.py
def average(numbers: list[int | float]) -> float:
    if not numbers:
        raise ValueError("average() requires at least one number")
    return sum(numbers) / len(numbers)

average(["a", "b", "c"])
average(["d", "e", "f"])

The type hints on the function declare that average() expects a list of integers or floating-point values and returns a floating-point value. However, both calls to average() are incorrect as they pass a list of strings instead of numbers.

Of course, executing the script will surface the first error:

Language: Shell
(.venv) $ python incorrect_types.py
Traceback (most recent call last):
  File "incorrect_types.py", line 6, in <module>
    average(["a", "b", "c"])
    ~~~~~~~^^^^^^^^^^^^^^^^^
  File "incorrect_types.py", line 4, in average
    return sum(numbers) / len(numbers)
           ~~~^^^^^^^^^
TypeError: unsupported operand type(s) for +: 'int' and 'str'

If you wanted Python to encounter the second error, you’d need to fix the first error and rerun the script. The program encounters these errors one at a time as it executes the code.

Instead, you can have Pyrefly find both type errors in your code at once using pyrefly check:

Language: Shell
(.venv) $ pyrefly check incorrect_types.py
ERROR Argument `list[str]` is not assignable to parameter `numbers`
⮑ with type `list[float | int]` in function `average` [bad-argument-type]
 --> incorrect_types.py:6:9
  |
6 | average(["a", "b", "c"])
  |         ^^^^^^^^^^^^^^^
  |
ERROR Argument `list[str]` is not assignable to parameter `numbers`
⮑ with type `list[float | int]` in function `average` [bad-argument-type]
 --> incorrect_types.py:7:9
  |
7 | average(["d", "e", "f"])
  |         ^^^^^^^^^^^^^^^
  |
 INFO 2 errors

Pyrefly can identify both errors without executing the code, which is why tools like this are referred to as _static_ type checkers. It analyzes your source code and compares the types it infers with the type hints you’ve declared. Unlike Python, which only surfaces errors as it executes problematic code, Pyrefly can identify type mismatches before the program runs.

To check every file in your project, run pyrefly check without a path. Pyrefly then uses the project-includes patterns from the pyrefly.toml configuration file:

Language: Shell
(.venv) $ pyrefly check

Static type checking should complement your other code checks, including your test suite, rather than replace them. Type checkers provide a way to verify that values passed between different parts of your code have compatible types. As your project grows, tools like Pyrefly can help you catch inconsistencies across functions, modules, and APIs before they make it into production.

Let Pyrefly Add Type Hints to Python Code

Writing type hints by hand for an untyped codebase takes time. Pyrefly can infer types from your code and add the matching type hints directly to your source files.

Type inference is the process of determining types from how values are used in a program rather than having a programmer explicitly declare them. Pyrefly follows the flow of values through expressions, function calls, assignments, and return statements to build up information about their types.

Consider the following function, which has no type hints:

Language: Python Filename: untyped.py
def average(numbers):
    if not numbers:
        raise ValueError("average() requires at least one number")
    return sum(numbers) / len(numbers)

average([1, 2.2, 3])

You can ask Pyrefly to infer the types with pyrefly infer. Do note that this will update the file in place:

Language: Shell
(.venv) $ pyrefly infer untyped.py

When you check the code in the file again, you’ll see that it now has type hints:

Language: Python Filename: untyped.py
def average(numbers: list[float | int]) -> float:
    if not numbers:
        raise ValueError("average() requires at least one number")
    return sum(numbers) / len(numbers)

average([1, 2.2, 3])

In this example, Pyrefly notes that the average() function was called with a list containing both float and int values, which is how it determines the type hint for the numbers parameter. It then examines the body of average() and notes that its only return statement produces a float, and thus concludes that average() must return a float.

Running pyrefly infer is most useful when you’re adding type hints to an existing codebase. Instead of type-hinting every function by hand, you can let Pyrefly handle the initial pass and then review its changes.

That said, automatic type inference isn’t a substitute for understanding the intent of your code. Pyrefly infers types based on the information available within the code itself. In the above example, average() was only ever called with a list as its input. However, you can successfully call this function with any container that has a length and supports iteration (collections.abc.Collection).

If these are the constraints you’d like to express in your code, you may type-hint the function accordingly:

Language: Python Filename: collection_average.py
from collections.abc import Collection

# A Collection is any object that has a length and can be iterated over
def average(numbers: Collection[int | float]) -> float:
    if not numbers:
        raise ValueError("average() requires at least one number")
    return sum(numbers) / len(numbers)

average([1, 2.2, 3])
average({1, 2.2, 3})
average((1, 2.2, 3))

The average() function above accepts any Collection that contains both int and float values. This means that the average() function is expected to appropriately handle lists, tuples, sets, dictionary keys, and much more. Compared with the inferred type hint, this one expresses that the function should “compute the average for any sized container that has floats and ints” rather than “compute the average of only lists containing floats and ints.”

Measure Your Type Coverage

Adding type hints to an existing project can be a gradual process. Pyrefly’s coverage command helps you measure how much type information is present in a codebase and identify functions that still need type hints.

Consider the following aggregations.py file:

Language: Python Filename: aggregations.py
from collections.abc import Iterable
from typing import Any

def average(numbers: list[int | float]) -> float:
    if not numbers:
        raise ValueError("average() requires at least one number")
    return sum(numbers) / len(numbers)

def median(values: Any, sort=False):
    values = sorted(values) if sort else values
    return values[(len(values) - 1) // 2]

def count(values: Iterable[int | None]):
    return sum(1 if v is not None else 0 for v in values)

The file contains three aggregation functions with different levels of type information. The average() function is fully type-hinted, while median() uses Any for its values parameter but leaves its sort parameter and return type unspecified. Finally, count() type-hints its parameter but leaves its return type unspecified.

It’s important to note that Any is compatible with every other type, which effectively switches off type checking for that value and provides very little information to the type checker. This gives Pyrefly a mix of fully typed, partially typed, and untyped items to report in its coverage results.

You can use pyrefly coverage check to see which functions still need type hints and to enforce a minimum coverage. The command below uses two options:

  • --fail-under 80 makes the check fail if fewer than 80 percent of the typable items are fully typed.
  • --strict counts items typed as Any as untyped.

Run the check on aggregations.py:

Language: Shell
(.venv) $ pyrefly coverage check aggregations.py --fail-under 80 --strict
 WARN `aggregations.median` is untyped [coverage-missing]
  --> aggregations.py:9:1
   |
 9 | / def median(values: Any, sort=False):
10 | |     values = sorted(values) if sort else values
11 | |     return values[(len(values) - 1) // 2]
   | |_________________________________________-
   |
 WARN `aggregations.count` is not fully typed [coverage-partial]
  --> aggregations.py:13:1
   |
13 | / def count(values: Iterable[int | None]):
14 | |     return sum(1 if v is not None else 0 for v in values)
   | |_________________________________________________________-
   |
ERROR strict type coverage 42.86% (3 of 7 typable) is below the 80.00% threshold

Pyrefly considers average() fully typed and count() partially typed. Under --strict, median() counts as untyped because its only type hint is Any. Across these functions, Pyrefly finds 7 typable items: 4 parameters and 3 return types. Three are fully typed, one uses Any, and three are untyped, resulting in 42.86% (3 of 7) strict type coverage. Without --strict, Any counts as typed, increasing coverage to 57.14% (4 of 7).

Coverage counts only the type hints written in your code, not what Pyrefly can infer. Pyrefly can tell that count() returns an int, but until you add -> int, that return type counts as untyped.

A high type coverage percentage only means that your code contains type hints. It doesn’t necessarily mean that those types usefully describe your code. For example, should average() accept only int and float values, as the Collection[int | float] version does, or should it also support fractions.Fraction and decimal.Decimal? The right answer depends on how you intend to use the function, which requires domain knowledge that a type checker can’t provide.

If you want more detail about the coverage status of your Python modules and functions, you can also use pyrefly coverage report to produce a JSON report with that information:

Language: Shell
(.venv) $ pyrefly coverage report aggregations.py
{
  "schema_version": "0.2",
  "module_reports": [
    {
      "name": "aggregations",
      ...
      "symbol_reports": [
        {
          "kind": "function",
          "name": "aggregations.average",
          "n_typable": 2,
          "n_typed": 2,
          "n_any": 0,
          "n_untyped": 0,
          ...
        },
        {
          "kind": "function",
          "name": "aggregations.median",
          "n_typable": 3,
          "n_typed": 0,
          "n_any": 1,
          "n_untyped": 2,
          ...
        },
        {
          "kind": "function",
          "name": "aggregations.count",
          "n_typable": 2,
          "n_typed": 1,
          "n_any": 0,
          "n_untyped": 1,
          ...
        }
      ],
      ...
    }
  ],
  "summary": {
    "n_modules": 1,
    "n_typable": 7,
    "n_typed": 3,
    "n_any": 1,
    "n_untyped": 3,
    "coverage": 57.14285714285714,
    "strict_coverage": 42.857142857142854,
    ...
  }
}

In the above output, each entry in symbol_reports describes one function. n_typable counts the items that can take a type hint, and n_typed, n_any, and n_untyped show how those items are covered. The summary adds these up for the whole run. Its coverage value counts Any as typed, while strict_coverage matches the 42.86% from the earlier --strict check.

The two coverage subcommands serve different purposes. pyrefly coverage check gives you a human-readable report and can enforce a minimum coverage threshold, making it great for CI systems, while pyrefly coverage report provides detailed JSON that you can use with other tools or dashboards. Together, they can help you both track and enforce static typing practices in a codebase by revealing where type hints are still missing.

Benchmark Pyrefly’s Performance

Speed is one of Pyrefly’s biggest selling points, particularly for large codebases. Pyrefly includes a benchmark script that compares its type-checking performance with other type checkers on actual Python projects.

If you want to try it yourself, this optional benchmark needs Git and a few minutes to run, since it downloads and type-checks each package. Clone the Pyrefly repository, then run its benchmark script. The -c option picks the type checkers, -n picks the packages, and -r sets the number of measured runs:

Language: Shell
(.venv) $ git clone --single-branch https://github.com/facebook/pyrefly.git
(.venv) $ python -m pip install mypy==2.3.1 pyright==1.1.411 pyrefly==1.2.0
(.venv) $ python pyrefly/scripts/benchmark/typecheck_benchmark.py \
            -c mypy pyright pyrefly -n requests flask django -r 1

======================================================================
Type Checker Timing Benchmark
======================================================================
Packages: 3
Type checkers: mypy, pyright, pyrefly
Timeout: 300s per checker
Warmup runs: 1
Measured runs: 1
======================================================================

Type Checker Versions:
  pyright: 1.1.411
  pyrefly: 1.2.0
  mypy: 2.3.1


[1/3] django
...
[2/3] flask
...
[3/3] requests
...

======================================================================
Benchmark Complete!
======================================================================

Checker       Pkgs  P50 Time  Avg Time  P95 Time  Max Time ...
----------------------------------------------------------
mypy             3      3.3s      5.0s      9.1s      9.8s ...
pyright          3      2.9s      8.4s     18.4s     20.1s ...
pyrefly          3      0.2s      0.5s      1.0s      1.1s ...
...

In this benchmark, Pyrefly has the fastest measured type-checking times of the three tools. Based on average times, Pyrefly is roughly ten times faster than mypy and nearly seventeen times faster than Pyright!

The comparison below races the three type checkers on those same numbers, so you can watch Pyrefly finish while the others are still working, and switch between the benchmark’s average and percentile timings:

Interactive diagram — enable JavaScript to view.

Each bar stops at that checker’s measured time for the selected metric, and the chip at its end shows the time in seconds. Switch to P95 or Max to compare the slowest packages, and click Replay race to watch the race again.

These results come from Pyrefly’s own benchmark, so they shouldn’t be treated as an independent performance comparison. The results can vary depending on the project, hardware, configuration, and versions of each type checker. Still, they give you an idea of why Pyrefly’s performance can be useful when type checking becomes a regular part of your development workflow.

Limitations to Know About

Pyrefly is a relatively new type checker, and there are two things to consider before adopting it:

  • Switching cost: If your project already uses mypy or Pyright, then switching to Pyrefly may require configuration changes or adjustments to existing type hints, which can create additional work. However, Pyrefly does provide migration guides to help with the transition.
  • Tooling compatibility: Other Python tools and projects sometimes specifically document or integrate with mypy or Pyright. This established ecosystem can matter when you’re building a workflow around a type checker.

These limitations don’t make Pyrefly a poor choice, but they’re worth considering alongside its features. For new projects or codebases adopting static typing, Pyrefly’s type checking, type-hint generation, and coverage features provide a useful combination of capabilities.

Conclusion

You’ve seen how Pyrefly can quickly check types, add type hints to existing code, and measure type coverage. These features cover much of the day-to-day work of introducing and maintaining static typing, but they’re only part of what Pyrefly offers. It also includes a language server for editor integration, configuration and migration support, and other tools for inspecting and working with your project.

Pyrefly is a strong fit if you’re adding type hints to an existing codebase or want fast type checks in CI. If your team already has a working mypy or Pyright setup, it’s worth a trial run, but not necessarily a switch.

In this tutorial, you learned how to:

  • Install Pyrefly and try it on a snippet with pyrefly snippet
  • Use pyrefly check to catch type errors before running your code
  • Use pyrefly infer to generate type hints for existing code
  • Use pyrefly coverage to measure and enforce type coverage across a project

If you’re interested in trying Pyrefly in your own projects, then check out the Pyrefly documentation for more commands and configuration options. You can also explore the Pyrefly language server documentation to add type checking to your editor. To brush up on type hints themselves, read Python Type Checking (Guide).

Take the Quiz: Test your knowledge with our interactive “Pyrefly: A Fast Static Type Checker for Python Projects” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Pyrefly: A Fast Static Type Checker for Python Projects

Test your understanding of Pyrefly, a fast Python type checker, from catching type errors to generating type hints and measuring type coverage.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Dictionary merging in Python 3.5+

About Cameron Riddell

Cameron is a Python trainer, consultant, and open source maintainer focused on data tools. He teaches Python, SQL, Git, and open science, and he loves the Python community. You'll often find him at events and meetups, so make sure to say hi!

» More about Cameron

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Topics: intermediate tools