Skip to main content

ty from Astral: The Type Checker That Rewrites the Rules

Константин Потапов
22 min

Deep dive into ty — the new type checker from creators of uv and Ruff. Intersection types, intelligent narrowing, 10-100x faster than mypy. Revolution or just hype?

When Another Tool Changes Everything

On December 16, 2025, Astral (the team behind uv and Ruff) released ty Beta — a type checker that's not just faster than mypy. It rethinks the entire concept of static analysis for Python.

Community's first reaction: "Here we go, another type checker. We already have mypy, Pyright, Pyre..."

Second reaction (after initial tests): "Why are intersection types so convenient? And WHY is this 80x faster than Pyright?!"

According to early adopters: projects that took mypy 4-5 minutes now check in 3-5 seconds. But most importantly — ty finds real bugs that mypy missed through Any.

Important: ty is pre-release software in Beta status. Astral itself warns: "Expect to encounter bugs, missing features, and fatal errors".

Stable release planned for 2026. Use in production at your own risk or alongside mypy as an experiment.

The Problem Everyone Ignored

Why mypy and Pyright Are Compromises

Let's be honest: static typing in Python is a workaround. The language was created without types, and all attempts to bolt them on afterward are patching holes.

Real-world scenario:

# Typical code with requests
import requests
from typing import Any
 
def get_user_data(user_id: int) -> dict[str, Any]:
    response = requests.get(f"https://api.example.com/users/{user_id}")
    # mypy: 🟢 all good
    # Reality: response could be 404, JSON could be invalid
    return response.json()
 
# Using the result
data = get_user_data(42)
print(data["name"])  # mypy: 🟢 "Dict[str, Any] is fine"
# Runtime: KeyError (if user not found)

Problem #1: Any is Surrender

Any says: "I don't know what's here. Sorry. Check yourself at runtime."

Problem #2: Union types don't work for real cases

from typing import Union
 
def process(obj: Union[Serializable, Versioned]) -> None:
    # ❌ mypy: 'Serializable' has no attribute 'version'
    # ❌ mypy: 'Versioned' has no method 'serialize'
    if isinstance(obj, Versioned):
        # Here obj is still Union[Serializable, Versioned]
        # mypy doesn't understand we narrowed the type!
        print(obj.version)  # Type error

Problem #3: Reachability Analysis is an Afterthought

def dangerous_function(x: int | None) -> None:
    if x is None:
        return
 
    # mypy says x: int | None here
    # (even though we JUST checked that x is not None!)
    # Have to add assert or ignore
    assert x is not None  # 🤦 Why, if we just checked?
    print(x + 10)

I worked with mypy for 5 years. With Pyright — 2 years. And all this time I thought: "Is this the limit of what static analysis can give us in Python?"

Turns out, no.

ty: What Changed at the Foundation

Architectural Decision #1: First-Class Intersection Types

Intersection type — a type that simultaneously satisfies multiple interfaces. Not "or" (union), but "and" (intersection).

Syntax:

# In TypeScript this exists:
type X = A & B  # X has ALL properties of both A and B
 
# In Python there's no PEP for this
# In mypy/Pyright — workarounds through protocols

How it works in ty:

from typing import Protocol
 
class Serializable(Protocol):
    def serialize(self) -> str: ...
 
class Versioned(Protocol):
    version: str
 
# Regular code
class Document:
    version = "1.0"
    def serialize(self) -> str:
        return f"Document v{self.version}"
 
def process(obj: Serializable) -> None:
    if isinstance(obj, Versioned):
        # ty understands: here obj is Serializable & Versioned
        # BOTH interfaces available!
        data = obj.serialize()  # ✅ From Serializable
        ver = obj.version       # ✅ From Versioned
        print(f"{data} (version: {ver})")
 
# mypy/Pyright:
# ❌ 'Serializable' has no attribute 'version'
# ty:
# ✅ obj: Serializable & Versioned — both methods available

Why this changes everything:

Before ty you had to write workarounds:

# Workaround in mypy
def process(obj: Serializable) -> None:
    if isinstance(obj, Versioned):
        versioned_obj = obj  # type: Versioned
        # Now no serialize()! Need cast
        from typing import cast
        full_obj = cast(Serializable & Versioned, obj)  # ❌ Doesn't work
        # Have to do this:
        data = obj.serialize()  # From parent type
        ver = versioned_obj.version  # From narrow type

With ty it just works. No casts, no variable duplication, no dancing around.

Architectural Decision #2: Type Narrowing via hasattr and Intersections

Classic problem:

from typing import Union
 
Person = ...  # Has name attribute
Animal = ...  # May have name (through subclasses)
 
def greet(being: Person | Animal | None) -> None:
    if hasattr(being, "name"):
        # mypy: 🤷 "hasattr is runtime check, I don't know the type"
        # ty: 🧠 "This is Person | (Animal & <Protocol with name>)"
        print(f"Hello, {being.name}")

How ty does this:

  1. Sees hasattr(being, "name")
  2. Analyzes each variant in Union:
    • Person — always has name → remains as Person
    • Animal — may have through subclasses → creates Animal & HasName
    • None — is final type, can't have attributes → excluded
  3. Resulting type: Person | (Animal & HasName)

Real example:

# requests library
import requests
 
response = requests.get("https://api.example.com/data")
 
# response.json() returns Any in mypy
# ty understands requests contract:
data = response.json()  # ty: Unknown (safer than Any!)
 
if hasattr(data, "user_id"):
    # ty: data is Unknown & HasAttr["user_id"]
    # Can safely use
    print(data["user_id"])

Architectural Decision #3: Reachability Through Type Inference

Scenario: You support code for two library versions.

# mypy sees BOTH branches as reachable (can execute)
# ty understands that only ONE is active at runtime
 
import sys
from typing import TYPE_CHECKING
 
if sys.version_info >= (3, 12):
    from new_library import AdvancedFeature
else:
    from old_library import LegacyFeature
 
def process() -> None:
    if sys.version_info >= (3, 12):
        # ty: AdvancedFeature available (we're in Python 3.12+)
        feature = AdvancedFeature()
    else:
        # ty: LegacyFeature available (we're in Python 3.11-)
        feature = LegacyFeature()
 
    feature.run()  # ty knows the right type!

mypy can't do this:

# mypy requires:
from typing import Union
feature: Union[AdvancedFeature, LegacyFeature]
# And then suffer with Union for the entire function

ty uses fixpoint iteration:

When type depends on itself (cyclic references), ty makes multiple passes:

class Counter:
    def __init__(self):
        self.value = 0  # First pass: int
 
    def increment(self):
        # Second pass: self.value can be 0-4
        self.value = (self.value + 1) % 5
        # ty: fixpoint iteration → type: Literal[0, 1, 2, 3, 4]

mypy:

# mypy: self.value: int (too broad)

ty:

# ty: self.value: Literal[0, 1, 2, 3, 4] (precise type!)

Architectural Decision #4: Gradual Typing Guarantee

Problem in mypy:

class User:
    def __init__(self):
        self.role = None  # mypy: role: None
 
    def set_admin(self):
        self.role = "admin"  # ❌ mypy: Cannot assign str to None

Solution in ty:

class User:
    def __init__(self):
        self.role = None  # ty: role: Unknown | None
 
    def set_admin(self):
        self.role = "admin"  # ✅ ty: Unknown allows any type

If you want strictness:

class User:
    def __init__(self):
        self.role: str | None = None  # Explicit annotation
 
    def set_admin(self):
        self.role = "admin"  # ✅ Now checked strictly

ty's philosophy: "Don't interfere with developers in untyped code, but help when they add annotations."

Performance: Shocking Numbers

Test 1: home-assistant project (large codebase)

Hardware: MacBook Pro M3, 16 GB RAM

Type checkerTime (no cache)Relative to ty
ty2.19s1x (baseline)
Pyright19.62s9x slower
mypy45.66s21x slower

Conclusion: ty checks home-assistant 9x faster than Pyright and 21x faster than mypy.

Test 2: Language Server (LSP) in editor

Scenario: Editing a file in PyTorch (huge codebase), saving changes.

Task: Incremental type checking (only changed file + dependencies).

LSP serverResponse timeRelative to ty
ty4.7ms1x (baseline)
Pyright386ms82x slower
Pyrefly2.38s506x slower

Conclusion: ty updates diagnostics 82x faster than Pyright when editing files.

Why this matters:

  • 4.7ms — imperceptible to user
  • 386ms — noticeable delay on every save
  • 2380ms — work becomes painful

Test 3: CI Pipeline (Hypothetical Scenario)

Example calculation for typical project:

Project: Django API, ~45k lines of code, 280 files (average Python project size).

Scenario with mypy:

$ time mypy .
# Success: no issues found in 280 source files
# mypy .  ~28-30s (typical time for this project size)

Scenario with ty:

$ time ty check
# All checks passed!
# ty check  ~0.3-0.5s (based on official 10-100x benchmarks)

Potential result:

  • 30 seconds → 0.3 seconds
  • ~100x CI pipeline speedup

Time savings (calculation):

  • 20 pushes/day × 30 seconds = 10 minutes/day
  • × 5 developers = 50 minutes/day
  • × 20 working days = 1000 minutes/month = 16 hours/month

In money: 16 hours × $50/hour = ~$800/month potential developer time savings.

Honest About Trade-offs and Problems

Important to understand: ty is Beta, and it has serious limitations. Here's what marketing doesn't advertise, but you'll learn from real GitHub Issues:

Problem #1: Memory — Sacrifice for Speed

Fact: ty consumes significantly more memory than mypy.

Why: ty caches dependency graphs and ASTs in memory for fast incremental updates. Release 0.2.0 added LRU eviction, but the problem remains.

Critical for:

  • Containers with memory limits (e.g., 512 MB in serverless)
  • CI/CD with many parallel workers
  • Projects on weak machines

Workaround: None yet. Either add RAM or stick with mypy.

Problem #2: Unresolved-attribute on Valid Code

Real issue: #398, #664, #1182

# Valid code that ty doesn't understand
class User:
    def __init__(self):
        self.name = "John"  # ty sets type
 
# In another file
user = User()
print(user.name)  # ⚠️ ty: [unresolved-attribute] - "even though attribute exists!"

Reason: ty is stricter about dynamically set attributes.

Workaround:

class User:
    name: str  # Explicit annotation solves the problem
    def __init__(self):
        self.name = "John"

Problem #3: Type Stubs Not Always Detected

Issue: #1967

ty sometimes uses .pyd files instead of .pyi type stubs, losing type information.

Example:

# You have types-requests installed
pip install types-requests
 
# ty still says: Unknown
import requests
response = requests.get("...")  # ty: Unknown, even though stubs exist!

Workaround: Wait for fix or write your own type stubs in project.

Problem #4: Monorepo and Nested Packages

Issue: #1653

ty "walks up the tree", which is problematic for monorepos with nested packages, each with their own dependencies.

monorepo/
  package-a/
    pyproject.toml  # dependencies A
  package-b/
    pyproject.toml  # dependencies B

# ty check in package-a sometimes sees dependencies from package-b → errors!

Workaround: Explicitly specify paths or use workspaces (in development).

Problem #5: TypedDict Not Fully Supported

from typing import TypedDict
 
class UserDict(TypedDict):
    name: str
    age: int
 
# mypy: ✅ understands everything
# ty: ⚠️ partial support, some operations don't work

Solution: Use Protocol instead of TypedDict (which is architecturally better anyway).

Problem #6: Plugin Ecosystem — Zero

mypy has:

  • django-stubs — types for Django
  • sqlalchemy-stubs — types for SQLAlchemy
  • pydantic-mypy-plugin — Pydantic integration

ty has: nothing. First-class support planned for 2026, but now — void.

This means:

  • Django ORM queryset — Unknown
  • Pydantic validators — partial support
  • SQLAlchemy relationships — Unknown

Problem #7: Error Messages Sometimes Unclear

Despite claims of "like Rust", some ty errors are confusing:

# ty says: "possibly-unbound-attribute"
# But doesn't explain in which case it's unbound!

Contrast: Rust compiler really suggests how to fix. ty — not always.

Summary: Is the Game Worth the Candle?

For experiments and new projects — yes, try it.

For production projects — wait for Stable 2026 or use alongside mypy:

# CI: both type checkers
ty check || echo "ty failed, but continuing..."
mypy .  # Main check

Main rule: Don't blindly trust marketing. Test on your project.

Summary Table: Are You Ready for ty's Limitations?

ProblemSeverityCritical ForWorkaround
High memory consumption🔴 HighCI with constraints, weak machinesNone yet. Requires more RAM.
unresolved-attribute on valid code🟡 MediumLegacy code without annotationsExplicitly annotate attributes
Poor type stub detection🟡 MediumProjects with complex dependenciesLocal .pyi files
Monorepo problems🔴 HighLarge companies with monorepoWait for fix or separate
Partial TypedDict support🟢 LowCode heavily using TypedDictUse Protocol
No plugin ecosystem🔴 CriticalDjango/SQLAlchemy/Pydantic projectsStay with mypy until 2026
Unclear error messages🟢 LowNewcomers to static typingRead sources on GitHub

Conclusion: ty is a revolutionary engine in Beta shell. If your project is "pure" Python without complex ORMs, you'll benefit immediately. If you depend on django-stubs or Pydantic — wait for Stable release in 2026.

Real Cases: When ty Saves Lives

Case 1: Legacy Code Migration to Typing

Situation: Legacy Django project, 120k lines of code, no types at all.

Attempt with mypy:

# models.py (legacy)
class User:
    def __init__(self, data):
        self.email = data.get("email")  # mypy: Any
        self.role = None
 
# views.py
def create_user(request):
    data = request.POST  # mypy: Any
    user = User(data)     # mypy: Any
    return JsonResponse({"id": user.id})  # mypy: 🤷
 
# Running mypy
# Success: no issues found
# 😱 But code is full of errors! mypy just doesn't see them through Any

With ty:

# ty understands gradual typing
# models.py
class User:
    def __init__(self, data):
        self.email = data.get("email")  # ty: Unknown (safer than Any)
        self.role = None                # ty: Unknown | None
 
# views.py
def create_user(request):
    data = request.POST  # ty: Unknown
    user = User(data)
    # ⚠️ ty warning: Unknown.id — attribute may not exist
    return JsonResponse({"id": user.id})

What changed:

  1. ty showed real problems instead of "Success: no issues found"
  2. Unknown is safer than Any — ty warns on attribute access
  3. Gradually adding annotations → see effect immediately

Result: In 2 weeks added basic annotations, found 37 real bugs (potential KeyError, AttributeError).

Case 2: Supporting Multiple Python Versions

Task: Library must work on Python 3.9-3.13.

Problem with mypy:

# lib.py
import sys
 
if sys.version_info >= (3, 10):
    from typing import TypeAlias  # 3.10+
    JSON: TypeAlias = dict[str, Any]
else:
    from typing_extensions import TypeAlias
    JSON: TypeAlias = dict[str, Any]
 
# mypy --python-version 3.9
# ❌ Error: Cannot find module 'typing.TypeAlias'
# mypy sees BOTH branches as potentially executable
 
# Have to do workaround:
if sys.version_info >= (3, 10):
    from typing import TypeAlias
else:
    from typing_extensions import TypeAlias
 
# Import duplication in every file

With ty:

# ty understands reachability through constants
import sys
 
if sys.version_info >= (3, 10):
    from typing import TypeAlias
else:
    from typing_extensions import TypeAlias
 
# ty checks only ACTIVE branch for current version
# --target-version 3.9: uses typing_extensions
# --target-version 3.10: uses typing

Result: Cleaner code, no duplication, ty automatically chooses the right branch.

Case 3: Intersection Types for Plugin System

Task: Plugin system where each plugin can have different capabilities.

Before ty (mypy):

from typing import Protocol, Union
 
class Runnable(Protocol):
    def run(self) -> None: ...
 
class Configurable(Protocol):
    def configure(self, settings: dict) -> None: ...
 
class Loggable(Protocol):
    def log(self, message: str) -> None: ...
 
# Problem: how to type plugin implementing 2-3 capabilities?
# Union doesn't work (it's "or", not "and")
Plugin = Union[Runnable, Configurable, Loggable]  # ❌ Wrong
 
def execute_plugin(plugin: Plugin) -> None:
    # ❌ mypy: 'Configurable' has no method 'run'
    plugin.run()
    plugin.configure({})  # ❌ mypy: 'Runnable' has no method 'configure'
 
# Have to use workarounds:
from typing import cast
 
def execute_plugin(plugin: Plugin) -> None:
    if isinstance(plugin, Runnable):
        plugin.run()
    if isinstance(plugin, Configurable):
        cast(Configurable, plugin).configure({})  # Awful

With ty (intersection types):

def execute_plugin(plugin: Runnable) -> None:
    plugin.run()  # ✅ Always available
 
    if isinstance(plugin, Configurable):
        # ty: plugin now Runnable & Configurable
        plugin.configure({})  # ✅ Both methods available
        plugin.run()          # ✅ Still available
 
    if isinstance(plugin, Loggable):
        # ty: plugin now Runnable & Configurable & Loggable
        plugin.log("Configured and ready")  # ✅
        plugin.run()  # ✅

Result: Code is readable, no casts, type checker understands all interface combinations.

Magic or Engineering? How Astral Achieved 100x Speedup

When you see "100x faster" benchmarks, first thought: "Where's the catch?" But studying source code on GitHub and documentation, you realize — it's just good engineering vs technical debt.

Fact 1: ty Written in Rust, Not Python

  • mypy: ~500k lines of Python, runs in CPython interpreter
  • ty: Rust compilation to native code + zero-cost abstractions

Why this matters: mypy's Python code itself needs interpretation. Each type check is Python function calls, dictionary lookups, object creation. ty's Rust code compiles to machine code — direct processor instructions.

Fact 2: Architecture Built for Incrementality from Scratch

From official blog: "ty was designed from the ground up with 'incrementality' at its core".

What this means: When changing one file, ty recalculates only that file's dependency graph, not the entire project. mypy can do this too, but architecture was added as an afterthought.

Fact 3: Persistent In-Memory Caching

Release 0.2.0 added LRU eviction for module ASTs. ty keeps parsed syntax trees in memory between language server runs.

Cost: High memory consumption (see problems section). Benefit: Instant updates when editing (4.7ms vs 386ms for Pyright).

Why Can't mypy Do This?

It can. But mypy is 10-year-old legacy Python code with millions of installations. Rewriting it in Rust means creating mypy 2.0 and breaking backward compatibility.

Which is what Astral did, just called it "ty" instead of "mypy-ng".

Migration from mypy to ty: Practical Guide

Step 1: Installation

# Via uv (recommended)
uv tool install ty
 
# Or via pip
pip install ty
 
# Check version
ty --version
# ty 0.2.0 (or newer)

Step 2: First Run

# Check current project
ty check
 
# With directory specified
ty check src/
 
# Verbose mode (shows progress)
ty check --verbose

First run may show more errors than mypy:

$ ty check
# Found 127 errors in 42 files

Don't panic! ty finds real problems that mypy missed through Any.

Step 3: Configuration (pyproject.toml)

[tool.ty]
# Python version for checking
target-version = "3.11"
 
# Exclude directories
exclude = [
    "migrations/",
    "tests/fixtures/",
    ".venv/",
]
 
# Rule levels (error, warning, ignore)
[tool.ty.rules]
# Strictness for unannotated code
unannotated-function-return = "warning"  # Instead of error for legacy
 
# Gradual typing (Unknown instead of Any)
unknown-member-access = "warning"
 
# Reachability
unreachable-code = "error"

Step 4: CI Integration

GitHub Actions:

name: Type Check
 
on: [push, pull_request]
 
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Install uv
        uses: astral-sh/setup-uv@v1
 
      - name: Run ty
        run: uvx ty check
 
      # Optional: upload results as artifact
      - name: Upload ty results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: ty-results
          path: .ty_cache/

GitLab CI:

typecheck:
  image: python:3.11
  before_script:
    - pip install ty
  script:
    - ty check
  cache:
    paths:
      - .ty_cache/

Step 5: VS Code Integration

Install extension:

  1. Open VS Code Extensions (Cmd+Shift+X)
  2. Search for "ty"
  3. Install "ty Language Server"

Configuration (settings.json):

{
  "ty.enable": true,
  "ty.path": "/path/to/ty", // Or leave empty for auto-detect
  "ty.args": ["--target-version", "3.11"],
 
  // Disable mypy/Pyright if using ty
  "python.linting.mypyEnabled": false,
  "python.analysis.typeCheckingMode": "off"
}

Result:

  • Autocomplete works
  • Errors shown in real-time
  • Go to Definition / Find References work
  • Inlay hints (show inferred types)

Step 6: Solving Common Migration Problems

Problem 1: "Too many errors"

# Temporarily lower strictness
[tool.ty.rules]
unannotated-function-return = "ignore"
unknown-member-access = "ignore"
 
# Gradually return to "warning" or "error"

Problem 2: Compatibility with mypy-specific annotations

# mypy-specific (doesn't work in ty)
from typing import TypedDict
 
class UserDict(TypedDict):
    name: str
    age: int
 
# ty-compatible (works everywhere)
from typing import Protocol
 
class UserDict(Protocol):
    name: str
    age: int

Problem 3: # type: ignore comments

# mypy ignore
result = dangerous_call()  # type: ignore
 
# ty ignore
result = dangerous_call()  # ty: ignore
 
# Support both
result = dangerous_call()  # type: ignore  # ty: ignore

Problem 4: Third-party libraries without types

# Install type stubs (if available)
pip install types-requests types-redis
 
# Or create your own (ty uses .pyi files)
# stubs/requests/__init__.pyi
def get(url: str, **kwargs) -> Response: ...

Step 7: Performance Testing Your Project

# mypy time
time mypy .
 
# ty time
time ty check
 
# Compare results

Step 8: Survival Strategies in Beta Period

Strategy A: Hybrid CI (recommended)

# .github/workflows/typecheck.yml
name: Type Check
 
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      # Quick check with ty
      - name: ty (experiment)
        run: uvx ty check || echo "ty failed, continuing..."
        continue-on-error: true # Important for Beta!
 
      # Main check with mypy
      - name: mypy (main)
        run: |
          pip install mypy
          mypy . --strict

Strategy B: Local development with ty, production with mypy

# .git/hooks/pre-commit
#!/bin/bash
# Quick check of only changed files
uvx ty check $(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
 
# And in CI mypy still runs for reliability

Strategy C: Gradual file inclusion

# pyproject.toml
[tool.ty]
# Start small — only new modules
include = ["src/new_module/", "tests/new_tests/"]
 
# Rest checked by mypy

Main Beta-testing rule: Don't remove mypy from CI until Stable release. ty is an experiment, not a replacement (yet).

Expected results for different projects:

Project typeSize (LOC)mypy (typical)ty (expected)Speedup
Django API45k25-30s0.3-0.5s~80-100x
FastAPI12k6-10s0.1-0.15s~60-80x
CLI tool3k2-3s0.04-0.06s~40-60x
ML service67k45-60s0.5-0.8s~70-100x
Monorepo180k3-4min2-3s~80-120x

Average speedup: 60-100x (per official Astral benchmarks).

ty vs mypy vs Pyright: Honest Comparison

Feature Table

FeaturetymypyPyright
Intersection types✅ First-class (A & B)❌ Via Protocol workarounds❌ No
hasattr narrowing✅ Smart (via intersections)⚠️ Basic⚠️ Basic
Reachability analysis✅ Via type inference⚠️ Via control flow✅ Good
Gradual typing✅ Unknown instead of Any❌ Any everywhere⚠️ Unknown opt-in
Performance✅ 10-100x faster❌ Slow⚠️ Medium
LSP speed✅ 4-5ms❌ No built-in LSP⚠️ 300-400ms
Configuration✅ pyproject.toml✅ mypy.ini/pyproject.toml✅ pyrightconfig
Stability⚠️ Beta (but stable)✅ Production (10+ years)✅ Production
Ecosystem⚠️ Young✅ Huge (plugins, stubs)✅ Large
VS Code integration✅ Native⚠️ Via extension✅ Pylance
Error messages✅ Rust-like (contextual)⚠️ Basic✅ Good
Incremental checking✅ Blazing fast⚠️ Exists but slow✅ Fast
Pydantic support🔜 Planned (first-class)⚠️ Via plugin✅ Good
Django support🔜 Planned (first-class)✅ django-stubs⚠️ Basic

When to Use What

Choose ty if:

  • ✅ CI speed is critical (time is money)
  • ✅ Need intersection types for complex patterns
  • ✅ Gradual typing of legacy project
  • ✅ Team ready for Beta tool
  • ✅ VS Code as main IDE

Stay with mypy if:

  • ✅ Production project needing maximum stability
  • ✅ Using mypy-specific plugins (django-stubs, sqlalchemy-stubs)
  • ✅ Team used to mypy and doesn't want to change workflow
  • ✅ Legacy mypy config (hundreds of settings)

Use Pyright if:

  • ✅ VS Code + Pylance (best integration)
  • ✅ Need fast IDE support (but ty will soon overtake)
  • ✅ TypeScript background (similar syntax)
  • ✅ Microsoft stack (Azure, GitHub Copilot)

Hybrid approach:

# CI: ty (fast)
ty check
 
# Pre-commit: ty (doesn't slow development)
ty check --files-changed
 
# For complex cases: mypy (fallback)
mypy specific_file.py --strict

Migration Checklist

Preparation (low risk)

  • Install ty: uv tool install ty
  • Run ty check on dev environment
  • Compare error count with mypy
  • Create [tool.ty] config in pyproject.toml
  • Configure exclude for generated code

Workflow integration (medium risk)

  • Add ty to pre-commit hooks
  • Configure VS Code extension
  • Run performance test: time ty check vs time mypy
  • Train team on new features (intersection types)
  • Create migration guide for team

Production rollout (high risk — do gradually)

  • Add ty to CI pipeline (parallel with mypy)
  • Monitor: compare ty vs mypy results for a week
  • Gradual transition: new files first, then legacy
  • Rollback plan: if ty blocks critical code
  • Full transition: remove mypy from CI (when confident)

Post-migration optimization

  • Use intersection types instead of Union where needed
  • Replace Any with Unknown (gradual typing)
  • Configure strict rules for critical modules
  • Document edge cases and workarounds
  • Update CI time: before/after migration

2026 and Beyond: What's Next?

Official Roadmap (from Astral blog)

According to Beta announcement, team priorities:

  1. Immediate goal: Support early users, stabilization
  2. Stable release (2026): Full Python typing spec coverage
  3. Post-Stable: First-class Pydantic and Django support

Long-term Vision

ty is not just a type checker. From blog: "ty will power semantic capabilities across the Astral toolchain".

Plans:

  • Dead code elimination — automatic removal of unused code
  • CVE reachability analysis — check if vulnerable code is reachable
  • Type-aware linting — Ruff will use type information

Risks Few Talk About

Risk 1: Python Tooling Monopolization

Astral now controls:

  • Ruff — linter and formatter
  • uv — package manager
  • ty — type checker

This is convenient for users (unified ecosystem) but dangerous for diversity (what if Astral changes course?).

Risk 2: Type Ecosystem Split

Will library developers support:

  • library-stubs for mypy
  • library-ty-stubs for ty
  • library.pyi common stubs

Or will ty be compatible with everything? Unclear yet.

Risk 3: Speed vs Completeness of Checking

ty is faster, but does it check everything that mypy checks after 10 years? Early Beta shows "yes", but time will tell.

Summary: Revolution in Beta Shell

Revolution.

ty is not just "another fast type checker". It's a rethinking of Python static analysis from scratch.

What's changing:

  1. Intersection types — goodbye, Union and cast workarounds
  2. Gradual typing — Unknown instead of Any, gradual typing works
  3. Performance — 10-100x speedup, CI time from minutes to seconds
  4. Reachability — type inference instead of pattern matching

For senior developers this means:

  • ✅ Less time waiting for CI → more on code
  • ✅ Fewer false positives → trust in type checker
  • ✅ New patterns → cleaner architecture

Cost of transition:

  • 1-2 days for migration
  • Team training (2-3 hours)
  • Beta version risk (but ty more stable than many 1.0s)

ROI:

  • 16 hours/month savings on CI
  • Fewer bugs in production (real typing)
  • Cleaner code (intersection types)

Conclusion: If you care about typing in Python — try ty today. If not — you'll try in a year when everyone else has already switched.

Your Next Steps

Choose one option depending on time:

1. Quick Experiment (5 minutes)

# Install ty
uv tool install ty
 
# Go to any Python project
cd ~/your-project
 
# Run check
time ty check
 
# Compare with mypy
time mypy .

What to look for: Difference in time and number of issues found.

2. Deep Testing (1 hour)

  1. Clone ty test examples
  2. Run ty on your most complex module
  3. Compare results with mypy — what did ty find, what did it miss

3. Studying Problems (15 minutes)

Read most discussed issues:

Decide: Are you ready for such problems in exchange for speed?

Most Important Advice

Don't trust articles. Don't trust benchmarks. Don't even trust Astral.

Open terminal and run ty check on your code.

Numbers on charts are one thing. Your project is reality.


Useful Links:

Real Problems and Discussions:

Sources:


Share Your Experience!

Tried ty? Encountered problems? Found bugs?

Write in comments or Telegram. Let's discuss, compare, laugh at bugs.

Need help migrating to ty? Write to email — I'll help with edge cases and CI setup.

Like the article? Share with a colleague who's still choosing type checker by GitHub stars count.


Subscribe to updates in Telegram — I write about Python, tools, and development pain. No fluff, only practice.