
Ty Skills
- 100 installs
- 7 repo stars
- Updated January 25, 2026
- jiatastic/open-python-skills
Python type checking with ty, Astral's fast Rust type checker: adding annotations, fixing errors, configuring rules, and migrating from mypy or pyright.
About
Covers Python type checking with ty, from running checks to advanced type patterns and editor setup. A developer uses it to add annotations, fix ty diagnostics, configure severity, or migrate from mypy/pyright.
- ty check CLI with configuration via pyproject.toml
- Advanced patterns: generics, protocols, intersection types; editor integration
Ty Skills by the numbers
- 100 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #105 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jiatastic/open-python-skills --skill ty-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 100 |
|---|---|
| repo stars | ★ 7 |
| Last updated | January 25, 2026 |
| Repository | jiatastic/open-python-skills ↗ |
What it does
Python type checking with ty, Astral's fast Rust type checker: adding annotations, fixing errors, configuring rules, and migrating from mypy or pyright.
Files
ty-skills
Master Python type checking with ty - the extremely fast type checker written in Rust by Astral (creators of uv and Ruff).
When to Use This Skill
- Adding type annotations to Python code
- Fixing type errors and diagnostics from ty
- Configuring ty rules and severity levels
- Migrating from mypy or pyright to ty
- Understanding advanced type patterns (intersection types, protocols, generics)
- Setting up ty language server in your editor
Quick Start
# Install
uv tool install ty
# or: pip install ty
# Check current directory
ty check
# Check specific files
ty check src/
# Full diagnostics
ty check --output-format fullConfiguration
Configure via pyproject.toml:
[tool.ty.environment]
python-version = "3.12"
python = "./.venv"
python-platform = "linux"
root = ["./src"]
extra-paths = ["./typings"]
[tool.ty.rules]
# error: fail CI, warn: report, ignore: disable
possibly-unresolved-reference = "error"
invalid-argument-type = "error"
division-by-zero = "warn"
unused-ignore-comment = "warn"
[tool.ty.src]
include = ["src", "tests"]
exclude = ["src/migrations/"]
# Per-file overrides
[[tool.ty.overrides]]
include = ["tests/**"]
[tool.ty.overrides.rules]
possibly-unresolved-reference = "warn"Rules Quick Reference
| Rule | Default | Description |
|---|---|---|
possibly-unresolved-reference | error | Variable might not be defined |
invalid-argument-type | error | Argument type mismatch |
incompatible-assignment | error | Assigned value incompatible |
missing-argument | error | Required argument missing |
unsupported-operator | error | Operator not supported for types |
invalid-return-type | error | Return type mismatch |
division-by-zero | warn | Potential division by zero |
unused-ignore-comment | warn | Suppression not needed |
redundant-cast | warn | Cast has no effect |
possibly-unbound-attribute | warn | Attribute might not exist |
index-out-of-bounds | warn | Index might be out of range |
Intersection Types (ty Exclusive)
ty has first-class intersection type support:
def output_as_json(obj: Serializable) -> str:
if isinstance(obj, Versioned):
reveal_type(obj) # reveals: Serializable & Versioned
return str({
"data": obj.serialize_json(), # From Serializable
"version": obj.version # From Versioned
})
return obj.serialize_json()Suppression Comments
# Suppress single rule
x: int = "hello" # type: ignore[incompatible-assignment]
# Suppress multiple
y = risky() # type: ignore[possibly-unresolved-reference, invalid-argument-type]Reference Documents
For detailed information, see:
| Document | Content |
|---|---|
references/ty_rules_reference.md | All rules with examples and fixes |
references/typing_cheatsheet.md | Python typing module quick reference |
references/advanced_patterns.md | Protocols, generics, type guards, variance |
references/migration_guide.md | mypy/pyright → ty migration |
references/common_errors.md | Error solutions with examples |
references/editor_setup/ | VS Code, Cursor, Neovim setup |
Resources
Advanced Type Patterns
Advanced typing patterns for complex Python codebases.
Intersection Types (ty Exclusive)
ty has first-class support for intersection types, which represent values that satisfy multiple type constraints simultaneously.
Basic Intersection
def output_as_json(obj: Serializable) -> str:
if isinstance(obj, Versioned):
reveal_type(obj) # reveals: Serializable & Versioned
# Can access members from BOTH types
return str({
"data": obj.serialize_json(), # From Serializable
"version": obj.version # From Versioned
})
return obj.serialize_json()Intersection with hasattr
class Person:
name: str
class Animal:
species: str
def greet(being: Person | Animal | None):
if hasattr(being, "name"):
# Type: Person | (Animal & <Protocol with 'name'>)
print(f"Hello, {being.name}!")
else:
print("Hello there!")Explicit Intersection Type
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ty_extensions import Intersection
type SerializableVersioned = Intersection[Serializable, Versioned]
def output_as_json(obj: SerializableVersioned) -> str:
# Can directly access both interfaces
return str({
"data": obj.serialize_json(),
"version": obj.version
})---
Protocol Patterns
Basic Protocol
from typing import Protocol
class Comparable(Protocol):
def __lt__(self, other: object) -> bool: ...
def __eq__(self, other: object) -> bool: ...
def find_min(items: list[Comparable]) -> Comparable:
return min(items)
# Works with any class implementing __lt__ and __eq__
find_min([3, 1, 2]) # ✅ int has these methods
find_min(["c", "a", "b"]) # ✅ str has these methodsProtocol with Properties
from typing import Protocol
class Named(Protocol):
@property
def name(self) -> str: ...
class Identifiable(Protocol):
@property
def id(self) -> int: ...
@property
def name(self) -> str: ...
def display(item: Named) -> None:
print(item.name)Generic Protocol
from typing import Protocol, TypeVar
T_co = TypeVar("T_co", covariant=True)
class Container(Protocol[T_co]):
def get(self) -> T_co: ...
class Box:
def __init__(self, value: int) -> None:
self._value = value
def get(self) -> int:
return self._value
def extract(container: Container[int]) -> int:
return container.get()
extract(Box(42)) # ✅ WorksCallback Protocol
from typing import Protocol
class EventHandler(Protocol):
def __call__(self, event: str, data: dict) -> None: ...
def register_handler(handler: EventHandler) -> None:
handler("startup", {})
# Lambda works
register_handler(lambda event, data: print(event))
# Function works
def my_handler(event: str, data: dict) -> None:
print(f"{event}: {data}")
register_handler(my_handler)
# Class with __call__ works
class LogHandler:
def __call__(self, event: str, data: dict) -> None:
print(f"[LOG] {event}")
register_handler(LogHandler())---
Variance
Covariance (Output Positions)
from typing import TypeVar, Generic
T_co = TypeVar("T_co", covariant=True)
class Producer(Generic[T_co]):
def get(self) -> T_co: ...
# Producer[Dog] is subtype of Producer[Animal]
# Because if you can produce Dogs, you can produce Animals
class Animal: pass
class Dog(Animal): pass
def use_producer(p: Producer[Animal]) -> Animal:
return p.get()
dog_producer: Producer[Dog] = ...
use_producer(dog_producer) # ✅ Covariance allows thisContravariance (Input Positions)
from typing import TypeVar, Generic
T_contra = TypeVar("T_contra", contravariant=True)
class Consumer(Generic[T_contra]):
def accept(self, item: T_contra) -> None: ...
# Consumer[Animal] is subtype of Consumer[Dog]
# Because if you can consume any Animal, you can certainly consume Dogs
def use_consumer(c: Consumer[Dog]) -> None:
c.accept(Dog())
animal_consumer: Consumer[Animal] = ...
use_consumer(animal_consumer) # ✅ Contravariance allows thisInvariance (Both Positions)
from typing import TypeVar, Generic
T = TypeVar("T") # Invariant by default
class MutableContainer(Generic[T]):
def get(self) -> T: ...
def set(self, value: T) -> None: ...
# MutableContainer[Dog] is NOT related to MutableContainer[Animal]
# Because you could put a Cat into a MutableContainer[Animal]---
Type Guard Patterns
Basic TypeGuard
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
"""Narrow list[object] to list[str]."""
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> str:
if is_string_list(items):
# items is now list[str]
return ", ".join(items)
return str(items)TypeGuard with Union
from typing import TypeGuard
class Success:
value: str
class Error:
message: str
type Result = Success | Error
def is_success(result: Result) -> TypeGuard[Success]:
return isinstance(result, Success)
def handle(result: Result) -> str:
if is_success(result):
return result.value # result is Success
return f"Error: {result.message}" # result is ErrorTypeIs (Python 3.13+)
from typing import TypeIs
def is_str(val: str | int) -> TypeIs[str]:
return isinstance(val, str)
def process(val: str | int) -> None:
if is_str(val):
print(val.upper()) # val is str
else:
print(val + 1) # val is int (properly narrowed)---
Decorator Patterns
Preserving Signatures with ParamSpec
from typing import TypeVar, ParamSpec, Callable
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def with_retry(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
for attempt in range(3):
try:
return func(*args, **kwargs)
except Exception:
if attempt == 2:
raise
raise RuntimeError("Unreachable")
return wrapper
@with_retry
def fetch_data(url: str, timeout: int = 30) -> dict:
...
# Type signature preserved!
fetch_data("https://api.example.com", timeout=60)Decorator that Changes Return Type
from typing import TypeVar, ParamSpec, Callable, Awaitable
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def make_async(func: Callable[P, R]) -> Callable[P, Awaitable[R]]:
@wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(*args, **kwargs)
return wrapper
@make_async
def compute(x: int, y: int) -> int:
return x + y
# Now returns Awaitable[int]
result = await compute(1, 2)Class Decorator
from typing import TypeVar, Type
T = TypeVar("T")
def singleton(cls: Type[T]) -> Type[T]:
instances: dict[Type[T], T] = {}
original_new = cls.__new__
def new_new(cls: Type[T], *args, **kwargs) -> T:
if cls not in instances:
instances[cls] = original_new(cls)
return instances[cls]
cls.__new__ = new_new # type: ignore
return cls
@singleton
class Database:
def __init__(self) -> None:
self.connected = False---
Factory Patterns
Generic Factory
from typing import TypeVar, Type
T = TypeVar("T")
def create(cls: Type[T], **kwargs) -> T:
return cls(**kwargs)
class User:
def __init__(self, name: str, email: str) -> None:
self.name = name
self.email = email
user = create(User, name="Alice", email="alice@example.com")
reveal_type(user) # UserAbstract Factory with Protocol
from typing import Protocol, TypeVar
class Connection(Protocol):
def execute(self, query: str) -> list: ...
class ConnectionFactory(Protocol):
def create(self) -> Connection: ...
def run_query(factory: ConnectionFactory, query: str) -> list:
conn = factory.create()
return conn.execute(query)---
Recursive Types
Self-Referential Type
from __future__ import annotations
class TreeNode:
def __init__(self, value: int) -> None:
self.value = value
self.children: list[TreeNode] = []
def add_child(self, child: TreeNode) -> TreeNode:
self.children.append(child)
return selfRecursive Type Alias
from typing import Union
# JSON type
type JSON = dict[str, JSON] | list[JSON] | str | int | float | bool | None
def parse_json(data: str) -> JSON:
import json
return json.loads(data)---
Overload Patterns
Return Type Based on Input
from typing import overload, Literal
@overload
def fetch(url: str, raw: Literal[True]) -> bytes: ...
@overload
def fetch(url: str, raw: Literal[False] = ...) -> str: ...
def fetch(url: str, raw: bool = False) -> bytes | str:
data = _download(url)
return data if raw else data.decode()
# Type-safe usage
text: str = fetch("https://example.com")
binary: bytes = fetch("https://example.com", raw=True)Overload with Optional
from typing import overload
@overload
def get_user(id: int) -> User: ...
@overload
def get_user(id: None) -> None: ...
def get_user(id: int | None) -> User | None:
if id is None:
return None
return User.fetch(id)
# Precise return types
user: User = get_user(42)
nothing: None = get_user(None)---
Context Manager Typing
from typing import Generator
from contextlib import contextmanager
@contextmanager
def managed_resource(name: str) -> Generator[Resource, None, None]:
resource = Resource(name)
try:
yield resource
finally:
resource.close()
# Async context manager
from typing import AsyncGenerator
from contextlib import asynccontextmanager
@asynccontextmanager
async def async_session() -> AsyncGenerator[Session, None]:
session = await Session.create()
try:
yield session
finally:
await session.close()Common ty Errors and Solutions
Practical solutions for frequently encountered ty type errors.
---
possibly-unresolved-reference
Error: Variable might not be defined in all code paths.
Example 1: Conditional Assignment
# ❌ Error
def get_status(success: bool) -> str:
if success:
message = "OK"
return message # 'message' possibly unresolved
# ✅ Solution 1: Default value
def get_status(success: bool) -> str:
message = "FAILED"
if success:
message = "OK"
return message
# ✅ Solution 2: Early return
def get_status(success: bool) -> str:
if success:
return "OK"
return "FAILED"Example 2: Loop Variable
# ❌ Error
def find_first(items: list[int], target: int) -> int:
for item in items:
if item == target:
found = item
break
return found # 'found' possibly unresolved
# ✅ Solution
def find_first(items: list[int], target: int) -> int | None:
for item in items:
if item == target:
return item
return NoneExample 3: Exception Handling
# ❌ Error
def parse_int(value: str) -> int:
try:
result = int(value)
except ValueError:
pass
return result # 'result' possibly unresolved
# ✅ Solution
def parse_int(value: str) -> int | None:
try:
return int(value)
except ValueError:
return None---
invalid-argument-type
Error: Argument type doesn't match the expected parameter type.
Example 1: Wrong Type
# ❌ Error
def greet(name: str) -> str:
return f"Hello, {name}"
greet(123) # Expected str, got int
# ✅ Solution: Convert or fix call site
greet(str(123))
greet("World")Example 2: None Where Not Expected
# ❌ Error
def process(data: str) -> str:
return data.upper()
value: str | None = get_value()
process(value) # Might be None
# ✅ Solution: Check for None
if value is not None:
process(value)
# Or use default
process(value or "default")Example 3: List vs Single Item
# ❌ Error
def process_item(item: str) -> None:
print(item)
items = ["a", "b", "c"]
process_item(items) # Expected str, got list[str]
# ✅ Solution: Iterate or fix function signature
for item in items:
process_item(item)---
incompatible-assignment
Error: Assigned value is incompatible with the declared type.
Example 1: Type Mismatch
# ❌ Error
x: int = "hello"
# ✅ Solution: Fix type or value
x: int = 42
# or
x: str = "hello"Example 2: Narrowing Issue
# ❌ Error
values: list[int] = [1, 2, 3]
values = None # list[int] can't be None
# ✅ Solution: Adjust type
values: list[int] | None = [1, 2, 3]
values = NoneExample 3: Dict Type Mismatch
# ❌ Error
config: dict[str, int] = {"name": "test"} # "test" is str, not int
# ✅ Solution
config: dict[str, str | int] = {"name": "test"}
# or
config: dict[str, str] = {"name": "test"}---
missing-argument
Error: Required argument not provided to function call.
Example 1: Missing Positional
# ❌ Error
def create_user(name: str, email: str) -> dict:
return {"name": name, "email": email}
create_user("Alice") # Missing 'email'
# ✅ Solution 1: Provide the argument
create_user("Alice", "alice@example.com")
# ✅ Solution 2: Make optional
def create_user(name: str, email: str | None = None) -> dict:
return {"name": name, "email": email}Example 2: Missing Keyword
# ❌ Error
def connect(host: str, port: int, timeout: int) -> Connection:
...
connect("localhost", 8080) # Missing 'timeout'
# ✅ Solution: Add default
def connect(host: str, port: int, timeout: int = 30) -> Connection:
...---
unsupported-operator
Error: Operator not supported for the given types.
Example 1: Type Incompatibility
# ❌ Error
result = "hello" + 42 # Can't add str and int
# ✅ Solution
result = "hello" + str(42)Example 2: None Type
# ❌ Error
value: int | None = get_value()
result = value + 10 # Can't add None and int
# ✅ Solution
if value is not None:
result = value + 10
# or
result = (value or 0) + 10Example 3: Custom Class
# ❌ Error
class Point:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2 # Point doesn't support +
# ✅ Solution: Implement __add__
class Point:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def __add__(self, other: "Point") -> "Point":
return Point(self.x + other.x, self.y + other.y)---
division-by-zero
Error: Potential division by zero detected.
# ⚠️ Warning
def average(numbers: list[int]) -> float:
return sum(numbers) / len(numbers) # len could be 0
# ✅ Solution 1: Check first
def average(numbers: list[int]) -> float:
if not numbers:
return 0.0
return sum(numbers) / len(numbers)
# ✅ Solution 2: Raise exception
def average(numbers: list[int]) -> float:
if not numbers:
raise ValueError("Cannot average empty list")
return sum(numbers) / len(numbers)---
possibly-unbound-attribute
Error: Attribute might not exist on the object.
Example 1: Optional Object
# ⚠️ Warning
user: User | None = get_user(id)
print(user.name) # user could be None
# ✅ Solution
if user is not None:
print(user.name)
# or
print(user.name if user else "Unknown")Example 2: Union Types
# ⚠️ Warning
class Dog:
name: str
class Cat:
nickname: str
def get_name(pet: Dog | Cat) -> str:
return pet.name # Cat doesn't have 'name'
# ✅ Solution 1: Check type
def get_name(pet: Dog | Cat) -> str:
if isinstance(pet, Dog):
return pet.name
return pet.nickname
# ✅ Solution 2: Common interface
class Pet(Protocol):
@property
def display_name(self) -> str: ...
def get_name(pet: Pet) -> str:
return pet.display_name---
redundant-cast
Error: Type cast has no effect (value already has target type).
from typing import cast
# ⚠️ Warning
x: int = 42
y = cast(int, x) # x is already int
# ✅ Solution: Remove cast
y = x
# Note: cast is only needed when you know more than the type checker
data: object = get_json()
if is_user_dict(data):
user = cast(dict[str, str], data) # Valid: narrowing from object---
unused-ignore-comment
Error: Suppression comment is unnecessary.
# ⚠️ Warning
x: int = 42 # type: ignore[incompatible-assignment] # No error here!
# ✅ Solution: Remove the comment
x: int = 42---
Pattern: Fixing Multiple Errors at Once
When you have many related errors, fix the root cause:
# ❌ Many errors
def process_data(data): # No type hints
result = data.split(",") # Unknown methods
for item in result:
handle(item) # Unknown function
# ✅ Add types to fix multiple errors at once
def process_data(data: str) -> None:
result: list[str] = data.split(",")
for item in result:
handle(item)
def handle(item: str) -> None:
print(item.strip())---
Debugging Tips
1. Use reveal_type
x = some_complex_expression()
reveal_type(x) # ty will show the inferred type2. Run with Full Output
ty check --output-format full3. Check Specific Files
ty check src/problematic_file.py4. Ignore Temporarily
# Fix later, don't block CI now
result = legacy_function() # type: ignore[possibly-unresolved-reference]Cursor Setup for ty
Configure ty as your Python type checker in Cursor.
Installation
1. ty Integration
Cursor has built-in support for ty. Enable it in settings:
1. Open Settings (Cmd+, / Ctrl+,) 2. Search for "ty" 3. Enable "Ty: Enable"
2. Install ty CLI
# Using uv (recommended)
uv tool install ty
# Or with pip
pip install tyConfiguration
settings.json
Open Settings → JSON and add:
{
// Enable ty
"ty.enable": true,
// Use project configuration
"ty.configPath": "${workspaceFolder}/pyproject.toml",
// Disable built-in type checking to avoid conflicts
"python.analysis.typeCheckingMode": "off"
}Project Configuration
Create or update pyproject.toml:
[tool.ty.environment]
python-version = "3.11"
python = "./.venv"
[tool.ty.rules]
possibly-unresolved-reference = "error"
invalid-argument-type = "error"
unused-ignore-comment = "warn"
[tool.ty.src]
include = ["src", "tests"]Features
Real-time Type Checking
ty checks your code as you type, showing:
- Errors - Red underlines for type violations
- Warnings - Yellow underlines for potential issues
- Inlay Hints - Inline type annotations
AI-Powered Fixes
Cursor's AI can help fix type errors:
1. Hover over a type error 2. Click "Fix with AI" or use Cmd+K / Ctrl+K 3. AI suggests a typed solution
Type-Aware Completions
ty enhances Cursor's autocomplete with:
- Type-accurate suggestions
- Method signatures with types
- Parameter hints
Keyboard Shortcuts
| Action | macOS | Windows/Linux |
|---|---|---|
| Go to Definition | Cmd+Click | Ctrl+Click |
| Type Definition | Cmd+F12 | Ctrl+F12 |
| Find References | Shift+F12 | Shift+F12 |
| Quick Fix | Cmd+. | Ctrl+. |
| AI Fix | Cmd+K | Ctrl+K |
Recommended Settings
{
// ty settings
"ty.enable": true,
"ty.configPath": "${workspaceFolder}/pyproject.toml",
// Disable conflicting features
"python.analysis.typeCheckingMode": "off",
// Enable inlay hints
"editor.inlayHints.enabled": "on",
// Ruff for formatting (pairs well with ty)
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}Using ty with Cursor AI
Type Error Fixes
When you see a type error:
1. Select the error line 2. Press Cmd+K / Ctrl+K 3. Ask: "Fix this type error"
Cursor AI understands ty's error messages and can suggest typed fixes.
Generate Type Annotations
Select untyped code and ask:
- "Add type annotations to this function"
- "What should the types be here?"
- "Make this code type-safe"
Explain Type Errors
Ask Cursor AI to explain:
- "Why is this a type error?"
- "What type does ty expect here?"
- "How do I fix this incompatible-assignment error?"
Troubleshooting
ty not detecting project
Ensure pyproject.toml or ty.toml is in the workspace root:
# Check if ty can find your config
ty check --show-configSlow Performance
For large projects, exclude unnecessary directories:
# pyproject.toml
[tool.ty.src]
exclude = [
"**/node_modules",
"**/.venv",
"**/build",
"**/dist"
]Conflicts with Other Type Checkers
Disable other Python type checkers:
{
"python.analysis.typeCheckingMode": "off",
"mypy.enabled": false,
"pylint.enabled": false
}Integration with Cursor Rules
Add ty-aware rules to .cursorrules:
# Type Checking
- All Python code must be type-safe with ty
- Use type annotations for function parameters and returns
- Fix type errors before committing
- Use `reveal_type()` to debug type inferenceNeovim Setup for ty
Configure ty as your Python type checker and language server in Neovim.
Installation
1. Install ty
# Using uv (recommended)
uv tool install ty
# Or with pip
pip install ty
# Verify
ty --version2. Configure Neovim
ty provides a language server. Configure it with your preferred LSP client.
nvim-lspconfig Setup
Basic Configuration
-- lua/plugins/ty.lua or init.lua
local lspconfig = require('lspconfig')
-- Register ty as a language server
local configs = require('lspconfig.configs')
if not configs.ty then
configs.ty = {
default_config = {
cmd = { 'ty', 'server' },
filetypes = { 'python' },
root_dir = lspconfig.util.root_pattern(
'pyproject.toml',
'ty.toml',
'.git'
),
settings = {},
},
}
end
-- Enable ty
lspconfig.ty.setup({
on_attach = function(client, bufnr)
-- Key mappings
local opts = { buffer = bufnr }
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts)
end,
})With mason.nvim
-- lua/plugins/mason.lua
return {
{
'williamboman/mason.nvim',
config = true,
},
{
'williamboman/mason-lspconfig.nvim',
dependencies = { 'mason.nvim' },
config = function()
require('mason-lspconfig').setup({
ensure_installed = { 'ty' }, -- When available in mason
})
end,
},
{
'neovim/nvim-lspconfig',
dependencies = { 'mason-lspconfig.nvim' },
config = function()
local lspconfig = require('lspconfig')
lspconfig.ty.setup({
-- Configuration here
})
end,
},
}LazyVim Configuration
If using LazyVim:
-- lua/plugins/ty.lua
return {
{
'neovim/nvim-lspconfig',
opts = {
servers = {
ty = {
cmd = { 'ty', 'server' },
filetypes = { 'python' },
},
},
},
},
}Disable Conflicting Servers
To avoid conflicts with pyright or pylsp:
-- Disable other Python LSPs when using ty
lspconfig.pyright.setup({
autostart = false, -- Don't auto-start pyright
})
-- Or use a condition
local python_lsp = vim.fn.executable('ty') == 1 and 'ty' or 'pyright'
if python_lsp == 'ty' then
lspconfig.ty.setup({})
else
lspconfig.pyright.setup({})
endKey Mappings
Recommended keybindings for ty:
-- After ty is attached
local on_attach = function(client, bufnr)
local opts = { buffer = bufnr, noremap = true, silent = true }
-- Navigation
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
vim.keymap.set('n', 'gt', vim.lsp.buf.type_definition, opts)
-- Information
vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts)
-- Refactoring
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts)
-- Diagnostics
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts)
vim.keymap.set('n', '<leader>e', vim.diagnostic.open_float, opts)
vim.keymap.set('n', '<leader>q', vim.diagnostic.setloclist, opts)
endDiagnostics Display
Configure diagnostic appearance:
vim.diagnostic.config({
virtual_text = {
prefix = '●',
source = 'if_many',
},
float = {
border = 'rounded',
source = 'always',
},
signs = true,
underline = true,
update_in_insert = false,
severity_sort = true,
})
-- Custom diagnostic signs
local signs = { Error = '', Warn = '', Hint = '', Info = '' }
for type, icon in pairs(signs) do
local hl = 'DiagnosticSign' .. type
vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
endIntegration with null-ls / none-ls
If you want ty as a diagnostic source:
local null_ls = require('null-ls')
null_ls.setup({
sources = {
null_ls.builtins.diagnostics.ty.with({
extra_args = { '--output-format', 'json' },
}),
},
})Telescope Integration
Find references and definitions with Telescope:
vim.keymap.set('n', 'gr', function()
require('telescope.builtin').lsp_references()
end, { desc = 'Find references' })
vim.keymap.set('n', 'gd', function()
require('telescope.builtin').lsp_definitions()
end, { desc = 'Go to definition' })
vim.keymap.set('n', '<leader>ds', function()
require('telescope.builtin').lsp_document_symbols()
end, { desc = 'Document symbols' })Troubleshooting
ty server not starting
Check if ty is in PATH:
which ty
ty server --helpCheck Neovim logs:
:LspLogSlow Startup
ty is fast, but for large projects:
# pyproject.toml
[tool.ty.src]
exclude = [
"**/node_modules",
"**/.venv",
"**/build"
]No Diagnostics
Ensure:
1. ty is running: :LspInfo 2. File type is Python: :set ft? 3. Project has pyproject.toml or ty.toml
Conflicts with Other LSPs
Only one Python LSP should be active:
-- Check active clients
:lua print(vim.inspect(vim.lsp.get_active_clients()))
-- Manually stop a client
:lua vim.lsp.stop_client(client_id)VS Code Setup for ty
Configure ty as your Python type checker in VS Code.
Installation
1. Install ty Extension
Search for "ty" in the VS Code extensions marketplace, or:
code --install-extension astral-sh.ty2. Install ty CLI
# Using uv (recommended)
uv tool install ty
# Or with pip
pip install tyConfiguration
settings.json
{
// Enable ty
"ty.enable": true,
// Path to ty executable (optional, auto-detected)
"ty.path": "",
// Path to configuration file (optional)
"ty.configPath": "./pyproject.toml",
// Disable Pylance type checking (to avoid conflicts)
"python.analysis.typeCheckingMode": "off",
// Or disable Pylance entirely
"python.languageServer": "None"
}Workspace Settings
Create .vscode/settings.json in your project:
{
"ty.enable": true,
"ty.configPath": "${workspaceFolder}/pyproject.toml",
// Disable Pylance type checking
"python.analysis.typeCheckingMode": "off",
// Keep Pylance for other features (completion, etc.)
"python.languageServer": "Pylance"
}Features
Diagnostics
ty provides inline diagnostics as you type:
- Errors (red squiggles) - Critical type issues
- Warnings (yellow squiggles) - Code quality issues
- Hints (blue dots) - Suggestions
Hover Information
Hover over variables to see:
- Inferred type
- Documentation
- Type narrowing context
Go to Definition
Ctrl+Click/Cmd+Clickon a symbolF12for Go to DefinitionCtrl+F12/Cmd+F12for Go to Type Definition
Find References
Shift+F12to find all references- Right-click → "Find All References"
Code Actions
- Quick fixes for type errors
- Auto-import suggestions
- Type annotation helpers
Keyboard Shortcuts
| Action | Windows/Linux | macOS |
|---|---|---|
| Go to Definition | F12 | F12 |
| Peek Definition | Alt+F12 | Option+F12 |
| Find References | Shift+F12 | Shift+F12 |
| Quick Fix | Ctrl+. | Cmd+. |
| Rename Symbol | F2 | F2 |
Recommended Extensions
For the best experience with ty:
{
"recommendations": [
"astral-sh.ty",
"charliermarsh.ruff", // Ruff for linting/formatting
"ms-python.python" // Python extension (for debugging, etc.)
]
}Troubleshooting
ty not found
If VS Code can't find ty:
{
"ty.path": "/path/to/ty"
}Find the path with:
which ty
# or
uv tool dirConflicts with Pylance
If you see duplicate diagnostics:
{
// Option 1: Disable Pylance type checking
"python.analysis.typeCheckingMode": "off",
// Option 2: Use ty as the only type checker
"python.languageServer": "None",
"ty.enable": true
}Performance Issues
For large projects:
{
// Exclude large directories
"ty.exclude": [
"**/node_modules",
"**/.venv",
"**/build"
]
}Wrong Python Version
Ensure ty uses the correct Python:
# pyproject.toml
[tool.ty.environment]
python-version = "3.11"
python = "./.venv"Tasks
Add ty to VS Code tasks:
// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "ty: Check",
"type": "shell",
"command": "ty check",
"group": "build",
"problemMatcher": {
"owner": "ty",
"pattern": {
"regexp": "^(.+):(\\d+):(\\d+): (error|warning): (.+)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
}
}
]
}Run with Ctrl+Shift+B (or Cmd+Shift+B on macOS).
Migration Guide: mypy/pyright → ty
Complete guide for migrating from mypy or pyright to ty.
Why Migrate to ty?
| Feature | mypy | pyright | ty |
|---|---|---|---|
| Speed | Slow | Fast | 10-100x faster |
| Language Server | Basic | Good | Excellent |
| Intersection Types | ❌ | ❌ | ✅ First-class |
| Incremental Analysis | Basic | Good | Fine-grained |
| Error Messages | Basic | Good | Rich contextual |
| Rust-based | ❌ | ❌ | ✅ |
---
Quick Migration Steps
1. Install ty
# Using uv (recommended)
uv tool install ty
# Or with pip
pip install ty
# Verify installation
ty --version2. Run Initial Check
# Check your project
ty check
# Get verbose output
ty check --output-format full3. Create Configuration
# ty will use pyproject.toml or ty.toml
# See configuration section below---
Configuration Migration
From mypy.ini
mypy.ini:
[mypy]
python_version = 3.11
strict = true
ignore_missing_imports = true
disallow_untyped_defs = true
disallow_any_generics = true
[mypy-tests.*]
disallow_untyped_defs = falseEquivalent ty (pyproject.toml):
[tool.ty.environment]
python-version = "3.11"
[tool.ty.rules]
# ty doesn't have a "strict" mode - configure individual rules
possibly-unresolved-reference = "error"
invalid-argument-type = "error"
incompatible-assignment = "error"
missing-argument = "error"
[tool.ty.src]
include = ["src", "tests"]
# Relaxed rules for tests
[[tool.ty.overrides]]
include = ["tests/**"]
[tool.ty.overrides.rules]
possibly-unresolved-reference = "warn"From pyrightconfig.json
pyrightconfig.json:
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__"],
"pythonVersion": "3.11",
"pythonPlatform": "Linux",
"typeCheckingMode": "strict",
"reportMissingImports": "error",
"reportUnusedImport": "warning"
}Equivalent ty (pyproject.toml):
[tool.ty.environment]
python-version = "3.11"
python-platform = "linux"
[tool.ty.src]
include = ["src"]
exclude = ["**/node_modules", "**/__pycache__"]
[tool.ty.rules]
# Map pyright rules to ty rules
possibly-unresolved-reference = "error"
possibly-unbound-import = "error"
unused-ignore-comment = "warn"---
Rule Mapping
mypy → ty
| mypy Error Code | ty Rule |
|---|---|
[arg-type] | invalid-argument-type |
[return-value] | invalid-return-type |
[assignment] | incompatible-assignment |
[call-arg] | missing-argument |
[operator] | unsupported-operator |
[attr-defined] | possibly-unbound-attribute |
[name-defined] | possibly-unresolved-reference |
[import] | possibly-unbound-import |
[unused-ignore] | unused-ignore-comment |
[redundant-cast] | redundant-cast |
pyright → ty
| pyright Rule | ty Rule |
|---|---|
reportArgumentType | invalid-argument-type |
reportReturnType | invalid-return-type |
reportAssignmentType | incompatible-assignment |
reportCallIssue | missing-argument |
reportOperatorIssue | unsupported-operator |
reportAttributeAccessIssue | possibly-unbound-attribute |
reportUndefinedVariable | possibly-unresolved-reference |
reportMissingImports | possibly-unbound-import |
reportUnusedIgnore | unused-ignore-comment |
reportUnnecessaryCast | redundant-cast |
---
Comment Syntax Migration
mypy comments
# mypy
x: int = "hello" # type: ignore[assignment]
y = data # type: ignore[name-defined]# ty - same syntax works!
x: int = "hello" # type: ignore[incompatible-assignment]
y = data # type: ignore[possibly-unresolved-reference]pyright comments
# pyright
x: int = "hello" # pyright: ignore[reportAssignmentType]
y = data # pyright: ignore# ty
x: int = "hello" # type: ignore[incompatible-assignment]
y = data # type: ignoreNote: ty uses # type: ignore[rule-name] syntax, same as mypy.
---
Handling Differences
1. Strictness Modes
mypy and pyright have --strict modes. ty uses individual rule configuration:
# ty "strict" equivalent
[tool.ty.rules]
possibly-unresolved-reference = "error"
invalid-argument-type = "error"
incompatible-assignment = "error"
missing-argument = "error"
unsupported-operator = "error"
division-by-zero = "error"
possibly-unbound-attribute = "error"
possibly-unbound-import = "error"
[tool.ty.terminal]
error-on-warning = true2. Plugin System
mypy has plugins (e.g., mypy-django, pydantic-mypy). ty doesn't have plugins yet, but:
- Many patterns work without plugins due to better type inference
- Pydantic v2 has native typing support
- Django stubs work as-is
# Instead of mypy plugins, use extra-paths for stubs
[tool.ty.environment]
extra-paths = ["./typings", "./stubs"]3. Custom Type Stubs
# mypy
[mypy]
mypy_path = stubs
# ty
[tool.ty.environment]
extra-paths = ["./stubs"]4. Inline Type Comments
ty supports inline type comments for Python 2 compatibility:
# Both work in ty
x = [] # type: list[int]
x: list[int] = []---
Gradual Migration Strategy
Phase 1: Parallel Running
Run both type checkers during transition:
# .github/workflows/ci.yml
jobs:
typecheck:
steps:
- name: mypy (existing)
run: mypy src/
continue-on-error: true # Don't block on mypy
- name: ty (new)
run: ty check src/Phase 2: ty as Primary
jobs:
typecheck:
steps:
- name: ty
run: ty check src/
# Optional: keep mypy for comparison
- name: mypy (verification)
run: mypy src/
continue-on-error: truePhase 3: ty Only
jobs:
typecheck:
steps:
- name: ty
run: ty check---
Common Migration Issues
Issue 1: Different Error Locations
ty and mypy/pyright may report errors at different locations:
def greet(name: str) -> str:
return name
greet(123) # mypy: error on this line
# ty: also error here, but different message formatSolution: Focus on fixing the underlying issues, not matching line numbers.
Issue 2: More/Fewer Errors
ty may catch errors that mypy/pyright miss (due to intersection types and better narrowing), or vice versa.
# ty catches this due to better narrowing
def process(x: int | None) -> int:
if x:
return x
# ty: error - missing return for x=0 caseSolution: These are usually real bugs. Fix them.
Issue 3: Different Inference
# mypy infers: list[Any]
# ty infers: list[int]
x = [1, 2, 3]Solution: ty's inference is usually more precise. This rarely causes issues.
---
CI/CD Updates
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@v4
- name: Install ty
run: uv tool install ty
- name: Type check
run: ty checkPre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: ty
name: ty type check
entry: ty check
language: system
types: [python]
pass_filenames: false---
Editor Migration
VS Code
Remove mypy/pylance extension, install ty extension:
// .vscode/settings.json
{
"python.analysis.typeCheckingMode": "off", // Disable pylance type checking
"ty.enable": true
}Cursor
ty is built into Cursor. Just configure:
{
"ty.enable": true,
"ty.configPath": "./pyproject.toml"
}See editor_setup/ for more editor configurations.
---
Cleanup Checklist
After successful migration:
- [ ] Remove
mypy.iniorpyrightconfig.json - [ ] Remove mypy/pyright from
requirements.txtorpyproject.toml - [ ] Update CI/CD configuration
- [ ] Update pre-commit hooks
- [ ] Update editor settings
- [ ] Update
# type: ignorecomments to use ty rule names - [ ] Remove mypy plugins from configuration
- [ ] Update contributing guidelines
ty Rules Reference
Complete reference for all ty rules and their configuration.
Rule Severity Levels
| Level | Behavior |
|---|---|
error | Reported as error, ty exits with code 1 |
warn | Reported as warning, ty exits with code 0 (unless --error-on-warning) |
ignore | Rule completely disabled |
Configuration
Via pyproject.toml
[tool.ty.rules]
possibly-unresolved-reference = "error"
division-by-zero = "warn"
unused-ignore-comment = "ignore"Via ty.toml
[rules]
possibly-unresolved-reference = "error"
division-by-zero = "warn"Via CLI
ty check --error possibly-unresolved-reference --warn division-by-zero---
Error Rules (Critical)
possibly-unresolved-reference
Default: error Description: Variable might not be defined in all code paths.
# ❌ Error
def greet(condition: bool) -> str:
if condition:
name = "World"
return f"Hello, {name}" # 'name' possibly unbound
# ✅ Fix
def greet(condition: bool) -> str:
name = "Guest" # Default value
if condition:
name = "World"
return f"Hello, {name}"invalid-argument-type
Default: error Description: Argument type doesn't match the expected parameter type.
# ❌ Error
def greet(name: str) -> str:
return f"Hello, {name}"
greet(123) # Expected str, got int
# ✅ Fix
greet("World")
# or
greet(str(123))incompatible-assignment
Default: error Description: Assigned value is incompatible with the declared type.
# ❌ Error
x: int = "hello"
# ✅ Fix
x: int = 42
# or
x: str = "hello"missing-argument
Default: error Description: Required argument not provided to function call.
# ❌ Error
def create_user(name: str, email: str) -> dict:
return {"name": name, "email": email}
create_user("Alice") # Missing 'email'
# ✅ Fix
create_user("Alice", "alice@example.com")
# or make email optional
def create_user(name: str, email: str | None = None) -> dict:
...unsupported-operator
Default: error Description: Operator not supported for the given types.
# ❌ Error
"hello" + 42 # Can't add str and int
# ✅ Fix
"hello" + str(42)invalid-return-type
Default: error Description: Return value doesn't match declared return type.
# ❌ Error
def get_count() -> int:
return "42" # Returns str, expected int
# ✅ Fix
def get_count() -> int:
return 42possibly-unbound-import
Default: error Description: Import might fail at runtime.
# ❌ Error
try:
import optional_package
except ImportError:
pass
optional_package.do_something() # Might not be defined
# ✅ Fix
try:
import optional_package
HAS_OPTIONAL = True
except ImportError:
optional_package = None
HAS_OPTIONAL = False
if HAS_OPTIONAL and optional_package:
optional_package.do_something()---
Warning Rules (Quality)
division-by-zero
Default: warn Description: Potential division by zero detected.
# ⚠️ Warning
def divide(a: int, b: int) -> float:
return a / b # b could be 0
# ✅ Fix
def divide(a: int, b: int) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / bunused-ignore-comment
Default: warn Description: Suppression comment is unnecessary.
# ⚠️ Warning
x: int = 42 # type: ignore[incompatible-assignment] # No error here!
# ✅ Fix - Remove unnecessary comment
x: int = 42redundant-cast
Default: warn Description: Type cast has no effect (value already has target type).
from typing import cast
# ⚠️ Warning
x: int = 42
y = cast(int, x) # x is already int
# ✅ Fix
y = xpossibly-unbound-attribute
Default: warn Description: Attribute might not exist on the object.
# ⚠️ Warning
def process(obj: Animal | None) -> str:
return obj.name # obj could be None
# ✅ Fix
def process(obj: Animal | None) -> str:
if obj is None:
return "Unknown"
return obj.nameindex-out-of-bounds
Default: warn Description: Index might be out of range.
# ⚠️ Warning
def get_first(items: list[int]) -> int:
return items[0] # Empty list would fail
# ✅ Fix
def get_first(items: list[int]) -> int | None:
return items[0] if items else Nonepossibly-missing-attribute
Default: warn Description: Attribute access might fail.
possibly-missing-import
Default: warn Description: Module might not be importable.
---
Suppression Comments
Suppress Single Rule
x: int = "hello" # type: ignore[incompatible-assignment]Suppress Multiple Rules
result = risky() # type: ignore[possibly-unresolved-reference, invalid-argument-type]Suppress All (Not Recommended)
legacy_result = legacy_code() # type: ignoreFile-Level Suppression
# ty: ignore[possibly-unbound-attribute]
# This comment at the top suppresses the rule for the entire file---
Per-File Overrides
# Relaxed rules for tests
[[tool.ty.overrides]]
include = ["tests/**"]
[tool.ty.overrides.rules]
possibly-unresolved-reference = "warn"
invalid-argument-type = "warn"
# Strict rules for critical code
[[tool.ty.overrides]]
include = ["src/payments/**", "src/auth/**"]
[tool.ty.overrides.rules]
division-by-zero = "error"
possibly-unbound-attribute = "error"---
Gradual Adoption Strategy
Phase 1: Critical Errors Only
[tool.ty.rules]
# Start with only truly critical errors
division-by-zero = "error"
invalid-argument-type = "warn"
incompatible-assignment = "warn"
# Disable noisy rules initially
possibly-unresolved-reference = "ignore"
redundant-cast = "ignore"Phase 2: Enable Warnings
[tool.ty.rules]
division-by-zero = "error"
invalid-argument-type = "error"
incompatible-assignment = "error"
possibly-unresolved-reference = "warn"
unused-ignore-comment = "warn"Phase 3: Full Strictness
[tool.ty.rules]
possibly-unresolved-reference = "error"
invalid-argument-type = "error"
incompatible-assignment = "error"
missing-argument = "error"
unsupported-operator = "error"
division-by-zero = "error"
unused-ignore-comment = "warn"
redundant-cast = "warn"
[tool.ty.terminal]
error-on-warning = truePython Typing Cheatsheet
Quick reference for Python's typing module and type annotations.
Basic Types
# Primitives
x: int = 42
y: float = 3.14
z: str = "hello"
flag: bool = True
data: bytes = b"binary"
nothing: None = NoneContainer Types (Python 3.9+)
# Use lowercase built-in types
numbers: list[int] = [1, 2, 3]
mapping: dict[str, int] = {"a": 1, "b": 2}
unique: set[str] = {"a", "b"}
frozen: frozenset[int] = frozenset([1, 2])
# Tuple - fixed length
point: tuple[int, int] = (10, 20)
# Tuple - variable length
values: tuple[int, ...] = (1, 2, 3, 4)Optional and Union (Python 3.10+)
# Optional (can be None)
name: str | None = None
# Union (one of multiple types)
id: int | str = "abc123"
# Pre-3.10 syntax (still valid)
from typing import Optional, Union
name: Optional[str] = None
id: Union[int, str] = "abc123"Callable Types
from typing import Callable
# Function with no args returning int
getter: Callable[[], int]
# Function with args
adder: Callable[[int, int], int]
# Any callable
handler: Callable[..., None]
# Example
def apply(func: Callable[[int], int], value: int) -> int:
return func(value)Type Aliases
# Simple alias (Python 3.12+)
type UserId = int
type UserDict = dict[str, str | int]
# Pre-3.12 syntax
from typing import TypeAlias
UserId: TypeAlias = int
UserDict: TypeAlias = dict[str, str | int]Generics
TypeVar
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# Constrained TypeVar
Number = TypeVar("Number", int, float)
def add(a: Number, b: Number) -> Number:
return a + bGeneric Classes
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# Usage
stack: Stack[int] = Stack()
stack.push(42)ParamSpec (Python 3.10+)
from typing import ParamSpec, TypeVar, Callable
P = ParamSpec("P")
R = TypeVar("R")
def with_logging(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapperProtocols (Structural Subtyping)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
# Circle is a Drawable even without explicit inheritance
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # ✅ WorksRuntime Checkable Protocol
from typing import Protocol, runtime_checkable
@runtime_checkable
class Sized(Protocol):
def __len__(self) -> int: ...
# Can use with isinstance
items = [1, 2, 3]
if isinstance(items, Sized):
print(len(items))Type Guards
TypeGuard (Python 3.10+)
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> None:
if is_string_list(items):
# items is now list[str]
print(", ".join(items))TypeIs (Python 3.13+)
from typing import TypeIs
def is_str(val: object) -> TypeIs[str]:
return isinstance(val, str)
def process(val: int | str) -> None:
if is_str(val):
print(val.upper()) # val is str
else:
print(val + 1) # val is intLiteral Types
from typing import Literal
Mode = Literal["r", "w", "a"]
def open_file(path: str, mode: Mode) -> None:
...
open_file("data.txt", "r") # ✅
open_file("data.txt", "x") # ❌ ErrorFinal and ClassVar
from typing import Final, ClassVar
class Config:
# Class variable (not instance variable)
instances: ClassVar[int] = 0
# Cannot be reassigned
VERSION: Final = "1.0.0"
def __init__(self) -> None:
Config.instances += 1Self Type (Python 3.11+)
from typing import Self
class Builder:
def set_name(self, name: str) -> Self:
self.name = name
return self # Returns same type, even in subclasses
class AdvancedBuilder(Builder):
def set_extra(self, extra: str) -> Self:
self.extra = extra
return self
# Chaining works correctly
builder = AdvancedBuilder().set_name("test").set_extra("data")Annotated
from typing import Annotated
# Add metadata to types (for validators, docs, etc.)
UserId = Annotated[int, "positive integer representing user ID"]
Email = Annotated[str, "valid email address"]
def get_user(user_id: UserId, email: Email) -> dict:
...Never and NoReturn
from typing import Never, NoReturn
# Function that never returns normally
def fail(message: str) -> NoReturn:
raise RuntimeError(message)
# Type that has no valid values (Python 3.11+)
def unreachable() -> Never:
raise AssertionError("This should never be called")Type Narrowing
def process(value: int | str | None) -> str:
# isinstance narrows type
if isinstance(value, str):
return value.upper() # value is str
# None check narrows type
if value is None:
return "default" # value is None
# Remaining type
return str(value) # value is intOverloads
from typing import overload
@overload
def parse(data: str) -> dict: ...
@overload
def parse(data: bytes) -> dict: ...
@overload
def parse(data: None) -> None: ...
def parse(data: str | bytes | None) -> dict | None:
if data is None:
return None
if isinstance(data, bytes):
data = data.decode()
return {"parsed": data}Forward References
from __future__ import annotations # Enable postponed evaluation
class Node:
# Can reference Node before it's fully defined
def add_child(self, child: Node) -> None:
...
# Or use string literal
class Tree:
def get_root(self) -> "Tree":
...Common Patterns
Factory Function
from typing import TypeVar, Type
T = TypeVar("T")
def create(cls: Type[T], **kwargs) -> T:
return cls(**kwargs)Context Manager
from typing import ContextManager
from contextlib import contextmanager
@contextmanager
def open_resource(name: str) -> ContextManager[Resource]:
resource = Resource(name)
try:
yield resource
finally:
resource.close()Async Types
from typing import AsyncIterator, Awaitable
from collections.abc import Coroutine
async def fetch() -> str:
return "data"
async def stream() -> AsyncIterator[int]:
for i in range(10):
yield i
# Awaitable
task: Awaitable[str] = fetch()