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 errorProblem #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 protocolsHow 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 availableWhy 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 typeWith 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:
- Sees
hasattr(being, "name") - Analyzes each variant in Union:
Person— always hasname→ remains asPersonAnimal— may have through subclasses → createsAnimal & HasNameNone— is final type, can't have attributes → excluded
- 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 functionty 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 NoneSolution 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 typeIf you want strictness:
class User:
def __init__(self):
self.role: str | None = None # Explicit annotation
def set_admin(self):
self.role = "admin" # ✅ Now checked strictlyty'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 checker | Time (no cache) | Relative to ty |
|---|---|---|
| ty | 2.19s | 1x (baseline) |
| Pyright | 19.62s | 9x slower |
| mypy | 45.66s | 21x 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 server | Response time | Relative to ty |
|---|---|---|
| ty | 4.7ms | 1x (baseline) |
| Pyright | 386ms | 82x slower |
| Pyrefly | 2.38s | 506x 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
# 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 workSolution: Use Protocol instead of TypedDict (which is architecturally better anyway).
Problem #6: Plugin Ecosystem — Zero
mypy has:
django-stubs— types for Djangosqlalchemy-stubs— types for SQLAlchemypydantic-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 checkMain rule: Don't blindly trust marketing. Test on your project.
Summary Table: Are You Ready for ty's Limitations?
| Problem | Severity | Critical For | Workaround |
|---|---|---|---|
| High memory consumption | 🔴 High | CI with constraints, weak machines | None yet. Requires more RAM. |
unresolved-attribute on valid code | 🟡 Medium | Legacy code without annotations | Explicitly annotate attributes |
| Poor type stub detection | 🟡 Medium | Projects with complex dependencies | Local .pyi files |
| Monorepo problems | 🔴 High | Large companies with monorepo | Wait for fix or separate |
| Partial TypedDict support | 🟢 Low | Code heavily using TypedDict | Use Protocol |
| No plugin ecosystem | 🔴 Critical | Django/SQLAlchemy/Pydantic projects | Stay with mypy until 2026 |
| Unclear error messages | 🟢 Low | Newcomers to static typing | Read 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 AnyWith 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:
- ty showed real problems instead of "Success: no issues found"
- Unknown is safer than Any — ty warns on attribute access
- 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 fileWith 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 typingResult: 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({}) # AwfulWith 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 --verboseFirst run may show more errors than mypy:
$ ty check
# Found 127 errors in 42 filesDon'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:
- Open VS Code Extensions (Cmd+Shift+X)
- Search for "ty"
- 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: intProblem 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: ignoreProblem 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 resultsStep 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 . --strictStrategy 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 reliabilityStrategy C: Gradual file inclusion
# pyproject.toml
[tool.ty]
# Start small — only new modules
include = ["src/new_module/", "tests/new_tests/"]
# Rest checked by mypyMain 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 type | Size (LOC) | mypy (typical) | ty (expected) | Speedup |
|---|---|---|---|---|
| Django API | 45k | 25-30s | 0.3-0.5s | ~80-100x |
| FastAPI | 12k | 6-10s | 0.1-0.15s | ~60-80x |
| CLI tool | 3k | 2-3s | 0.04-0.06s | ~40-60x |
| ML service | 67k | 45-60s | 0.5-0.8s | ~70-100x |
| Monorepo | 180k | 3-4min | 2-3s | ~80-120x |
Average speedup: 60-100x (per official Astral benchmarks).
ty vs mypy vs Pyright: Honest Comparison
Feature Table
| Feature | ty | mypy | Pyright |
|---|---|---|---|
| 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 --strictMigration Checklist
Preparation (low risk)
- Install ty:
uv tool install ty - Run
ty checkon 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 checkvstime 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
AnywithUnknown(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:
- Immediate goal: Support early users, stabilization
- Stable release (2026): Full Python typing spec coverage
- 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-stubsfor mypylibrary-ty-stubsfor tylibrary.pyicommon 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:
- Intersection types — goodbye, Union and cast workarounds
- Gradual typing — Unknown instead of Any, gradual typing works
- Performance — 10-100x speedup, CI time from minutes to seconds
- 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)
- Clone ty test examples
- Run ty on your most complex module
- Compare results with mypy — what did ty find, what did it miss
3. Studying Problems (15 minutes)
Read most discussed issues:
- #398 - unresolved-attribute — revealing bug
- #1653 - monorepo support — architectural problem
- #1967 - stub files — ecosystem integration
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 checkon your code.Numbers on charts are one thing. Your project is reality.
Useful Links:
- ty Official Blog — official Beta release announcement
- ty Documentation — complete documentation
- ty GitHub Repository — source code and issues
- ty Type System Guide — intersection types details
- Talk Python To Me #506 — podcast with Charlie Marsh (May 1, 2025)
- Charlie Marsh on X — tweets about ty
- Simon Willison's Analysis — analysis from known developer
Real Problems and Discussions:
- GitHub Issues - ty — bug tracker (read before migration!)
- Issue #398 — unresolved-attribute problems
- Issue #1653 — monorepo support
- Issue #1967 — type stub detection
Sources:
- ty: An extremely fast Python type checker and LSP - Astral
- Python type checker ty now in beta | InfoWorld
- ty Documentation - Astral
- Type system features - ty Docs
- Talk Python Podcast Episode #506
- GitHub Issues - astral-sh/ty
- ty releases - GitHub
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.

