Skip to content

Does Ruff support type checking?

No. Ruff is a Python linter and formatter; it does not perform type inference and is not a type checker. The official Ruff FAQ states this directly: “Ruff is a linter, not a type checker. It can detect some of the same problems that a type checker can, but a type checker will catch certain errors that Ruff would miss.”

For type checking, pair Ruff with a dedicated type checker. The handbook covers four in detail: ty, mypy, Pyright, and Pyrefly. See How do Python type checkers compare? for the trade-offs.

Why people think Ruff does type checking

A few features of Ruff and its ecosystem create the impression that it might do more than it does.

Astral builds Ruff and ty as separate projects

Astral ships Ruff and ty as separate projects with separate binaries. The two share infrastructure (the same Rust crate for parsing Python source, for example) but install and run independently. ruff check runs the linter; ty check runs the type checker. Installing Ruff does not give you type checking, and using ty does not require Ruff.

The naming and shared authorship create a reasonable expectation that “Astral’s Python tool” handles everything, but the split is intentional. Linting and type checking are different categories of static analysis with different trade-offs around speed, configuration, and false positives.

Ruff’s ANN, TC, and PYI rules read annotation syntax

Several of Ruff’s rule categories read type annotations as syntax:

  • ANN (flake8-annotations): flags functions that are missing annotations on arguments or return values.
  • TC (flake8-type-checking): identifies imports that are used only inside annotations and suggests moving them into if TYPE_CHECKING: blocks.
  • PYI (flake8-pyi): enforces conventions for .pyi stub files.

These rules look at the text of the annotation, not at whether the annotation is correct. A function annotated def add(a: int, b: int) -> str and returning a + b passes every Ruff rule even though the return type is wrong. A type checker catches the mismatch; Ruff does not look at it.

Pyflakes rules look like type checks but aren’t

Ruff inherits the pyflakes rule set, which catches undefined names (F821), unused imports (F401), and unused variables (F841). These checks operate on the abstract syntax tree without doing type inference. They overlap with what a type checker would flag in some cases, which makes Ruff feel more “type-aware” than it actually is.

The difference: pyflakes notices that a name was never assigned. A type checker notices that an assigned name has the wrong type.

What a type checker catches that Ruff misses

A concrete example shows the gap:

def parse_count(raw: str) -> int:
    return raw.strip()  # bug: .strip() returns str, not int

total: int = parse_count("  42  ")
print(total + 1)  # TypeError at runtime

Ruff reports no errors on this code. Every name is defined, every import is used, every annotation parses. The function declares it returns int, and that declaration is syntactically valid.

A type checker walks the function body, infers that raw.strip() returns str, and reports the type error before runtime:

$ ty check parse.py
error[invalid-return-type]: Return type does not match returned value
 --> parse.py:1:30
  |
1 | def parse_count(raw: str) -> int:
  |                              --- Expected `int` because of return type
2 |     return raw.strip()
  |            ^^^^^^^^^^^ expected `int`, found `str`

mypy, Pyright, and Pyrefly report the same mismatch with slightly different output formats. Catching this category of bug requires tracing values through expressions, which is what type inference does and what Ruff does not do.

Pair Ruff with a dedicated type checker

For new projects, the handbook recommends running Ruff for lint and format, and one of the four major type checkers for type analysis:

  • ty (Astral, Rust, beta as of 2026): fast, prioritizes inference on unannotated code, integrates with the Astral toolchain.
  • mypy (community, Python, 1.0 since 2023): the original Python type checker; widest plugin ecosystem.
  • Pyright (Microsoft, TypeScript, available since 2019): the engine behind Pylance; strongest VS Code integration.
  • Pyrefly (Meta, Rust, 1.0 in May 2026): fast, high spec-conformance, used at Instagram and PyTorch.

How do Python type checkers compare? walks through the trade-offs (speed, conformance, strictness defaults, licensing). For a step-by-step playbook on adding type checking to an existing codebase, see How to gradually adopt type checking in an existing Python project.

A common project setup runs all of these tools at different stages: Ruff on save, the type checker on commit (via prek), and both in CI.

Learn More

Last updated on