Intermediate

You add type hints to your Python code — function signatures, return types, the works — and then run mypy and wait. And wait. On a large codebase, that wait can stretch to 30 seconds or more, which means you stop running the type checker constantly and start running it only before commits. At that point, you have already written the buggy code, refactored around it, and half-forgotten what you were thinking when you typed it.

ty is Astral’s Python type checker, written in Rust by the same team behind ruff and uv. It runs 10x to 100x faster than mypy and Pyright, which means type checking becomes a tight feedback loop instead of a CI-only ritual. You install it with a single command, run it the same way you would run any linter, and get results before you have time to switch tabs.

This article walks you through the full ty workflow: installation, your first type check, reading and understanding its output, configuring it for a real project, suppressing false positives cleanly, and integrating it into your editor and CI pipeline. By the end you will know exactly how to make ty part of every Python project you start.

ty Quick Example

Before we go deep, here is a minimal example of what ty does. Save this file, run one command, and see type errors reported in milliseconds.

# check_me.py

def greet(name: str) -> str:
    return "Hello, " + name

def add(x: int, y: int) -> int:
    return x + y

# These calls have type errors
result = greet(42)          # Wrong: passing int where str expected
total = add("one", "two")   # Wrong: passing str where int expected
print(result, total)

Now run ty against the file:

uvx ty check check_me.py

Output:

check_me.py:10:15: error[invalid-argument-type] Argument of type `int` is not assignable to parameter `name` of type `str` in function `greet`
check_me.py:11:14: error[invalid-argument-type] Argument of type `str` is not assignable to parameter `x` of type `int` in function `add`

Found 2 errors in 1 file.

ty pinpoints the exact line and column of each problem, names the error rule (invalid-argument-type), and explains what went wrong in plain language. Each error includes the inferred type, the expected type, and the function involved. No configuration file needed — it works out of the box.

What Is ty and Why Use It?

A type checker reads your Python source code without running it. It traces how values flow through your program — from function calls to return values to assignments — and flags places where the types do not match. This catches whole categories of bugs at write time rather than runtime: passing a None where a string is expected, calling a method that does not exist on a particular type, returning the wrong type from a function.

Python has had type hints since version 3.5 (PEP 484), but actually using them required running a separate slow checker and waiting for results. ty changes the economics: because it is written in Rust, it can check even large codebases in under a second. That speed changes how you use it — from an occasional gate to a constant companion.

ToolLanguageSpeed (vs mypy)Status (2026)
mypyPython1x (baseline)Mature, widely used
PyrightTypeScript5-10x fasterMature, powers Pylance
PyreflyRust10-60x fasterMeta’s checker, newer
tyRust10-100x fasterAstral beta, growing fast

ty is backed by Astral, the team behind ruff (the fastest Python linter) and uv (the fastest Python package manager). If you already use those tools, ty fits naturally into the same workflow.

Python type checker speed comparison - ty vs mypy performance
Mypy: 30 seconds. ty: 300ms. Same bugs caught.

Installing ty

There are three ways to install ty, depending on how you manage your Python tools. The fastest method — and the one that needs no prior setup — is uvx, which runs ty in an isolated environment without installing anything permanently.

Run Without Installing (uvx)

If you have uv installed, you can run ty immediately without a separate install step:

# run_ty_uvx.sh

# Run ty against all Python files in the current directory
uvx ty check

# Run against a specific file
uvx ty check src/app.py

# Run against a specific directory
uvx ty check src/

Output (first run — downloads ty automatically):

Installed 1 package in 312ms
No errors found.

After the first run, uvx caches the package so subsequent invocations are instant. This is the recommended way to use ty in CI pipelines because the exact version is pinned automatically.

Install Permanently

For a permanent installation that puts ty on your PATH, use uv tool install:

# install_ty.sh

# Install ty as a global tool (requires uv)
uv tool install ty

# Or install via pip into the current environment
pip install ty

# Verify the installation
ty --version

Output:

ty 0.0.0-alpha.14

Once installed globally, you can run ty check from any directory. Use pip install ty if you are adding it to a project’s requirements-dev.txt or pyproject.toml dev dependencies.

Installing ty Python type checker with one command
One command. Zero waiting. Your type checker is ready.

Reading ty’s Output

Before you start fixing errors, it helps to understand exactly what ty is telling you. The output format is consistent and information-dense.

# output_examples.py
from typing import Optional

def process_user(user_id: int) -> dict:
    return {"id": user_id, "active": True}

def get_name(user: dict) -> str:
    return user["name"]

# ty will flag this -- Optional[int] might be None
def format_count(count: Optional[int]) -> str:
    return "Count: " + str(count * 2)   # Error: count could be None

# ty will flag this -- can't multiply str by str
def repeat_word(word: str, times: str) -> str:
    return word * times   # Error: times should be int, not str

Output:

output_examples.py:12:25: error[possibly-unbound-implicit-call] Cannot call `__mul__` on `None`
output_examples.py:12:25: error[operator] Operator `*` not supported for types `int | None` and `int`
output_examples.py:16:12: error[operator] Operator `*` not supported for types `str` and `str`

Found 3 errors in 1 file.

Each line follows the same pattern: file:line:column: severity[rule-code] message. The rule-code in brackets (like operator or invalid-argument-type) is important because it is what you use in suppression comments or configuration files to control which rules run.

Type Checking a Real Project

Running ty check with no arguments checks all Python files in your current directory and its subdirectories. This is the most common way to use it on a real project.

# check_project.sh

# Check all Python files in the project
ty check

# Check with verbose output (shows which files were checked)
ty check --verbose

# Check and output as JSON (useful for CI scripts)
ty check --output-format json

# Check only errors, skip warnings
ty check --error-on warning

Output (example project with multiple files):

src/api/routes.py:23:9: error[invalid-argument-type] Argument of type `list[str]` is not assignable to parameter `ids` of type `list[int]`
src/models/user.py:45:16: error[attribute-access] Object of type `Optional[str]` has no attribute `upper`; use `str | None`
src/utils/parse.py:78:12: error[return-type] Return type `None` is not assignable to declared return type `str`

Found 3 errors in 47 files checked in 0.4s

The final line shows how many files were checked and how long it took. On a project with dozens of files, that 0.4 second time is typical — fast enough to run on every file save.

ty scanning Python project files for type errors
47 files. 0.4 seconds. That one red file owes you an explanation.

Configuring ty

For most projects you will want to tell ty a few things: which Python version to target, which directories to skip, and which rules to enable or disable. All of this goes in pyproject.toml or a dedicated ty.toml file.

Using pyproject.toml

The most common setup adds a [tool.ty] section to your existing pyproject.toml:

# pyproject.toml

[tool.ty]
# Target Python version for type checking
python-version = "3.11"

# Directories to exclude from type checking
exclude = [
    "tests/fixtures/",
    "migrations/",
    "docs/",
    "__pycache__/",
]

# Set rule severities: "error", "warn", "info", "ignore"
[tool.ty.rules]
# Treat these as warnings instead of errors during migration
possibly-undefined = "warn"
missing-argument = "warn"

Save that file in your project root, then run ty check and it will pick up the configuration automatically. You do not need to pass any flags — ty finds pyproject.toml by walking up from the current directory, the same way ruff and uv do.

Using ty.toml (Standalone)

If you prefer to keep ty configuration separate from your build config, create a ty.toml file:

# ty.toml

python-version = "3.11"

exclude = [
    "tests/",
    "build/",
]

[rules]
# Promote these to errors for strict mode
return-type = "error"
attribute-access = "error"

The ty.toml file takes precedence over pyproject.toml‘s [tool.ty] section if both are present. Use whichever keeps your repo organized.

Suppressing False Positives

No type checker is perfect. Sometimes ty will flag something it cannot fully understand — a dynamic attribute set at runtime, a third-party library without type stubs, or a pattern that is technically correct but requires knowledge of your runtime environment. For these cases, ty supports inline suppression comments.

# suppression_examples.py

from typing import Any

# Suppress a specific rule on this line
value: Any = get_dynamic_value()  # type: ignore[return-type]

# Suppress all ty errors on this line (use sparingly)
result = legacy_api_call()  # type: ignore

# Suppress for a whole block using a rule name
def process_webhook_payload(data: dict) -> None:
    # The payload shape is validated at the API boundary, not here
    user_id: int = data["user"]["id"]  # type: ignore[assignment]
    send_notification(user_id)

# The preferred pattern: narrow the type explicitly instead of suppressing
def safe_process(data: dict) -> None:
    raw_id = data.get("user", {}).get("id")
    if not isinstance(raw_id, int):
        raise ValueError(f"Expected int user id, got {type(raw_id)}")
    send_notification(raw_id)  # ty knows raw_id is int here

Output (with suppression):

suppression_examples.py:21:5: note[assignment] Type `dict[str, Any] | int | None` is assignable to `int` via suppression
No errors found.

The rule name in type: ignore[rule-name] is optional but strongly recommended. Broad type: ignore comments silently suppress all errors on that line, including real bugs you introduce later. Specific suppressions document WHY you are overriding the checker — future you will be grateful.

Using type: ignore suppression comment in Python with ty
type: ignore[assignment] — because you know something ty doesn’t.

Editor Integration

The real productivity gain from ty comes from running it continuously in your editor. ty ships a language server (LSP) that provides inline diagnostics, hover documentation, auto-import, and code navigation — without any additional configuration beyond installing the extension.

VS Code

Install the official ty VS Code extension from the marketplace. Once installed, open any Python file and type errors appear as red underlines immediately — no terminal window needed.

# .vscode/settings.json
{
    "ty.enable": true,
    "ty.pythonVersion": "3.11",
    "[python]": {
        "editor.defaultFormatter": "charliermarsh.ruff",
        "editor.formatOnSave": true
    }
}

The extension uses the same configuration from pyproject.toml or ty.toml that your CLI invocations use, so there is no separate editor config to maintain.

Neovim / Other Editors

For any editor that supports the Language Server Protocol, configure ty as a language server:

# init.lua (Neovim with nvim-lspconfig)
require('lspconfig').ty.setup({
    cmd = { "ty", "server" },
    filetypes = { "python" },
    root_dir = require('lspconfig.util').root_pattern("pyproject.toml", "ty.toml", ".git"),
})

The ty server subcommand starts the LSP. After it is running, you get inline diagnostics, hover types, and go-to-definition for free.

Adding ty to CI

Type checking in CI catches regressions before they merge. Here is how to add ty to a GitHub Actions workflow alongside your existing linters:

# .github/workflows/quality.yml
name: Code Quality

on: [push, pull_request]

jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v3

      - name: Set up Python
        uses: astral-sh/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: uv sync --dev

      - name: Run ty
        run: uvx ty check src/

      - name: Run ruff (linting)
        run: uvx ruff check src/

      - name: Run ruff (formatting)
        run: uvx ruff format --check src/

Output on a clean run:

No errors found. (47 files checked in 0.3s)

The ty check command exits with code 0 when there are no errors and code 1 when there are, so CI fails automatically on type errors with no extra configuration needed.

ty type checker running in CI pipeline for Python project
Exits 0 on clean. Exits 1 on errors. CI does the rest.

Migrating from mypy

If you have an existing project with mypy, switching to ty is usually straightforward. Both tools read standard Python type hints, so your existing annotations do not need to change. The main differences are in configuration file names and a handful of rule names that differ between the two tools.

# migrate_check.sh

# Step 1: Run ty against your codebase
ty check src/

# Step 2: Compare error counts
mypy src/ | tail -1          # e.g. "Found 12 errors in 5 files"
ty check src/ | tail -1      # e.g. "Found 9 errors in 5 files"

# ty may find fewer errors initially (different rules)
# and more errors (stricter on certain patterns)
# Treat discrepancies as a review task, not a blocker

Common mypy config equivalents in ty:

mypy settingty equivalent
python_version = "3.11"python-version = "3.11"
ignore_missing_imports = true[rules] unresolved-import = "warn"
strict = trueNo direct equivalent — enable rules individually
# type: ignore# type: ignore (same syntax, shared)
exclude = ["tests/"]exclude = ["tests/"]

A practical migration strategy: run both tools in parallel for a sprint, fix errors that appear in both, and then drop mypy once you are satisfied with ty‘s coverage on your codebase.

Real-Life Example: Type-Safe Data Processing Pipeline

Here is a realistic Python module that processes user records from a JSON API. It has several type annotations and a few deliberate type errors that ty will catch.

Type-safe data processing pipeline in Python with ty type checking
Runtime surprises are for amateurs. Compile time is for professionals.
# pipeline.py
from typing import Optional
import json

# --- Data models ---
class User:
    def __init__(self, user_id: int, name: str, email: str, age: Optional[int] = None):
        self.user_id = user_id
        self.name = name
        self.email = email
        self.age = age

    def display_name(self) -> str:
        return self.name.upper()

    def age_group(self) -> str:
        if self.age is None:
            return "unknown"
        if self.age < 18:
            return "minor"
        return "adult"


# --- Processing functions ---
def parse_users(raw_json: str) -> list[User]:
    data = json.loads(raw_json)
    users = []
    for record in data:
        user = User(
            user_id=record["id"],      # assumes int
            name=record["name"],       # assumes str
            email=record["email"],     # assumes str
            age=record.get("age"),     # Optional[int]
        )
        users.append(user)
    return users


def filter_adults(users: list[User]) -> list[User]:
    return [u for u in users if u.age_group() == "adult"]


def format_report(users: list[User]) -> str:
    lines = []
    for user in users:
        line = f"ID={user.user_id} | {user.display_name()} | {user.email}"
        lines.append(line)
    return "\n".join(lines)


# --- Main ---
SAMPLE_DATA = json.dumps([
    {"id": 1, "name": "Alice Nguyen", "email": "alice@example.com", "age": 34},
    {"id": 2, "name": "Bob Chen",    "email": "bob@example.com",    "age": 15},
    {"id": 3, "name": "Carol Kim",   "email": "carol@example.com"},
])

users = parse_users(SAMPLE_DATA)
adults = filter_adults(users)
print(format_report(adults))

Output:

ID=1 | ALICE NGUYEN | alice@example.com
# Run ty to verify the module is type-clean
# run_ty.sh
ty check pipeline.py

ty Output:

No errors found. (1 file checked in 0.1s)

This module passes ty‘s checks cleanly because every function has annotated parameters and return types, Optional[int] is handled before use (via age_group()), and json.loads‘s return value flows through named variables with clear type assumptions. To extend this pipeline, add a save_report(path: str, content: str) -> None function and run ty check again — if you forget to pass the right types, ty will catch it before you run the code.

Frequently Asked Questions

What are the key differences between ty and mypy?

The most obvious difference is speed: ty is 10x to 100x faster than mypy on the same codebase, making it practical to run on every file save rather than only in CI. Beyond speed, ty has first-class support for intersection types and more advanced type narrowing — patterns that mypy handles inconsistently. ty is also stricter about certain patterns by default, such as unresolved imports and possibly-None access. During a migration, expect to find some errors that mypy missed and some mypy errors that ty does not flag, as the two tools have different rule sets.

What happens with third-party libraries that lack type stubs?

When ty cannot find type information for an import, it reports an unresolved-import error. For libraries that have official type stubs (like boto3-stubs, types-requests, etc.), install the stubs package and ty will pick them up automatically. For libraries without stubs, set unresolved-import = "warn" in your [tool.ty.rules] config to downgrade those from errors to warnings, or create a minimal stub file yourself using .pyi files.

How do I add ty to an existing codebase that has no type hints?

Start by running ty check and counting the errors. Then add a [tool.ty.rules] section to your pyproject.toml and set the noisiest rules to "warn" instead of "error". This lets you enable type checking without breaking CI on day one. Annotate the most critical modules first — API boundaries, data models, and utility functions — and gradually tighten the rules as coverage improves. Running ty check --stats shows which rules are generating the most noise, which guides prioritization.

Should I use ty or Pyright?

If you use VS Code and already have Pylance (which is powered by Pyright), you are already getting type checking inline. Switching to ty makes sense when you want a consistent tool across all editors and CI, when the check speed on your codebase has become a friction point, or when you are already using ruff and uv and want the same Astral ecosystem for type checking. ty is newer and still in beta, but Astral now uses it exclusively on their own projects. Pyright is more mature and has broader rule coverage for edge cases in the Python type system.

Which Python versions does ty support?

ty officially supports type checking code targeting Python 3.10 and later. You declare your target version via python-version in your config, and ty adjusts its understanding of built-in types accordingly — for example, it knows that list[int] is valid syntax in 3.10+ but requires List[int] from typing in earlier versions. If you need to support Python 3.8 or 3.9, configure the version in ty.toml and note that some newer typing features will be flagged as unavailable.

How does ty achieve such dramatic speed improvements?

The core reason is that ty is written in Rust, which eliminates Python’s interpreter overhead and allows aggressive parallelism across CPU cores. But the architecture matters too: ty uses fine-grained incremental analysis, meaning that when you edit one file, it only re-analyses the parts of the dependency graph affected by that change. When editing a file in the PyTorch repository (a large codebase), ty recomputes diagnostics in 4.7ms — 80x faster than Pyright and 500x faster than an older version of Pyrefly benchmarked in 2026. This makes the language server feel instant even in very large projects.

Conclusion

Type checking with ty takes what has historically been a slow, CI-only ritual and turns it into a tight development loop. The installation is one command (pip install ty or uv tool install ty), the CLI is a single command (ty check), and the speed — 10x to 100x faster than mypy — means you can run it on every save without thinking about it. The configuration in pyproject.toml or ty.toml is minimal: a Python version target, a list of excluded directories, and optional rule severity overrides for migrating gradually from an untyped codebase.

The best next step is to run ty check against a project you are already working on. If you get zero errors, add stricter rules one at a time. If you get hundreds of errors, downgrade the noisiest rules to "warn" in the config and fix them incrementally. The real-life pipeline example above shows what a cleanly typed module looks like — use it as a template for how to structure annotated functions and handle Optional values correctly before they cause runtime crashes.

The official ty documentation is at docs.astral.sh/ty and covers advanced topics including intersection types, module discovery, and the full rule reference. The GitHub repository tracks the current beta status and has a migration guide from mypy.