技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
python-type-safety【数据与研究】Modern Python type safety with type hints, generics, protocols, and strict type checking using ty and ruff. Use when adding type annotations, implementing generic classes, defining structural interfaces, configuring ty/ruff, or writing type-safe Python 3.13+/3.14+ code. Triggers on mentions of type hints, typing, TypeVar, Protocol, overload, TypeIs, …
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Modern Python type safety using the Astral toolchain: ty for type checking and language server, ruff for linting (including annotation enforcement), and uv for package management. All examples target Python 3.13+ with 3.14 deferred annotations as the default.
| Tool | Role | Install |
|---|---|---|
| ty | Type checker + LSP (10-100x faster than mypy) | uv tool install ty@latest |
| ruff | Linter + formatter (replaces flake8, black, isort) | uv tool install ruff |
| uv | Package manager + env management | astral.sh/uv |
ty is the type checker from Astral (the creators of ruff and uv). It is written in Rust, provides a full language server (LSP) with code navigation, completions, and inlay hints, and supports advanced features like intersection types, reachability analysis, and gradual typing guarantees.
def first[T](items: list[T]) -> T:type statement for aliases: type Point = tuple[float, float]class Box[T = int]:TypeIs for proper bidirectional type narrowingReadOnly for TypedDict itemswarnings.deprecated() decorator with type system supportfrom __future__ import annotations, no more forward reference strings)except and except* expressions may omit bracketsEvery public function, method, and class should have type annotations. Use native generics everywhere (no typing.List, typing.Dict, etc. — those are ancient history).
from pydantic import BaseModel
class User(BaseModel):
id: str
name: str
email: str | None = None
class UserRepository:
def __init__(self, db: Database) -> None:
self._db = db
async def find_by_id(self, user_id: str) -> User | None:
...
async def find_by_email(self, email: str) -> User | None:
...
async def save(self, user: User) -> User:
...
TypeIs provides proper bidirectional narrowing — when the guard returns False, the type checker narrows the else branch too (unlike the older TypeGuard).
from typing import TypeIs
def is_string(x: int | str) -> TypeIs[str]:
return isinstance(x, str)
def process(value: int | str) -> str:
if is_string(value):
return value.upper() # value: str
else:
return str(value * 2) # value: int (properly narrowed)
def is_valid_user(obj: dict | User) -> TypeIs[User]:
return isinstance(obj, User)
Use TypeIs over TypeGuard in all new code. TypeGuard only narrows the true branch; TypeIs narrows both.
Use the built-in type parameter syntax — no TypeVar boilerplate needed.
from pydantic import BaseModel
class Result[T, E: Exception]:
"""Either a success value or an error."""
def __init__(
self,
value: T | None = None,
error: E | None = None,
) -> None:
if (value is None) == (error is None):
raise ValueError("Exactly one of value or error must be set")
self._value = value
self._error = error
@property
def is_success(self) -> bool:
return self._error is None
def unwrap(self) -> T:
if self._error is not None:
raise self._error
return self._value # type: ignore[return-value]
def unwrap_or(self, default: T) -> T:
if self._error is not None:
return default
return self._value # type: ignore[return-value]
class ConfigError(Exception): ...
class Config(BaseModel):
host: str
port: int
def parse_config(path: str) -> Result[Config, ConfigError]:
try:
return Result(value=Config.model_validate_json(open(path).read()))
except Exception as e:
return Result(error=ConfigError(str(e)))
Generic parameters can now have defaults, reducing boilerplate for common cases.
class Response[T = dict]:
def __init__(self, data: T, status: int = 200) -> None:
self.data = data
self.status = status
# Uses default: Response[dict]
r1 = Response({"ok": True})
# Explicit: Response[list[str]]
r2 = Response(["a", "b"], status=201)
class Container[T = int]:
def __init__(self, value: T) -> None:
self.value = value
# Container[int] by default
c = Container(42)
Define interfaces without inheritance — duck typing with full type safety.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict: ...
@classmethod
def from_dict(cls, data: dict) -> "Serializable": ...
class Closeable(Protocol):
def close(self) -> None: ...
class AsyncCloseable(Protocol):
async def close(self) -> None: ...
class HasId(Protocol):
@property
def id(self) -> str: ...
# Any class with matching methods satisfies the protocol — no inheritance needed
class User:
def __init__(self, id: str, name: str) -> None:
self.id = id
self.name = name
def to_dict(self) -> dict:
return {"id": self.id, "name": self.name}
@classmethod
def from_dict(cls, data: dict) -> "User":
return cls(id=data["id"], name=data["name"])
def serialize(obj: Serializable) -> str:
import json
return json.dumps(obj.to_dict())
serialize(User("1", "Alice")) # Works — User matches Protocol
isinstance(User("1", "Alice"), Serializable) # True at runtime
In Python 3.14, annotations are evaluated lazily. Forward references just work — no string quoting, no from __future__ import annotations.
# This just works in 3.14+ — no special imports needed
class TreeNode[T]:
value: T
children: list[TreeNode[T]] # Forward reference, no quotes needed
def __init__(self, value: T) -> None:
self.value = value
self.children = []
def add_child(self, value: T) -> TreeNode[T]:
child = TreeNode(value)
self.children.append(child)
return child
# Mutually recursive types — also just work
class Order:
customer: Customer # Forward reference resolved lazily
total: float
class Customer:
orders: list[Order]
name: str
The new annotationlib module provides tools for introspecting deferred annotations when needed at runtime (VALUE, FORWARDREF, STRING formats).
Use Literal types for tagged unions with exhaustive pattern matching.
from typing import Literal
from pydantic import BaseModel
class EmailNotification(BaseModel):
type: Literal["email"] = "email"
to: str
subject: str
body: str
class SMSNotification(BaseModel):
type: Literal["sms"] = "sms"
to: str
message: str
class PushNotification(BaseModel):
type: Literal["push"] = "push"
device_token: str
title: str
body: str
type Notification = EmailNotification | SMSNotification | PushNotification
async def send_notification(notification: Notification) -> bool:
match notification.type:
case "email":
return await send_email(notification.to, notification.subject, notification.body)
case "sms":
return await send_sms(notification.to, notification.message)
case "push":
return await send_push(notification.device_token, notification.title, notification.body)
Preserve function signatures through decorators.
from typing import ParamSpec, Callable, Awaitable
from functools import wraps
import time
type P = ParamSpec # Can't use type statement for ParamSpec yet
# Use the traditional form for ParamSpec
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def timing(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def retry(
max_attempts: int = 3,
exceptions: tuple[type[Exception], ...] = (Exception,),
) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_exception: Exception | None = None
for _ in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
raise last_exception or RuntimeError("Retry failed")
return wrapper
return decorator
@retry(max_attempts=3, exceptions=(ValueError, IOError))
@timing
def fetch_data(url: str, timeout: int = 30) -> dict:
...
Note: Python 3.12 introduced def f[**P, R](...) syntax for ParamSpec in function signatures, but ParamSpec and TypeVar are still needed for module-level decorator definitions.
Return different types based on argument values.
from typing import overload, Literal
@overload
def fetch(url: str, *, as_json: Literal[True]) -> dict: ...
@overload
def fetch(url: str, *, as_json: Literal[False] = False) -> str: ...
def fetch(url: str, *, as_json: bool = False) -> dict | str:
import requests
response = requests.get(url)
if as_json:
return response.json()
return response.text
# Type checker knows the exact return type
data: dict = fetch("https://api.example.com", as_json=True)
text: str = fetch("https://api.example.com")
Type-safe data access using Pydantic models.
from typing import Protocol
from pydantic import BaseModel
from abc import abstractmethod
class Repository[T: BaseModel, ID](Protocol):
async def get(self, id: ID) -> T | None: ...
async def get_all(self) -> list[T]: ...
async def save(self, entity: T) -> T: ...
async def delete(self, id: ID) -> bool: ...
class User(BaseModel):
id: str
name: str
email: str
class SQLRepository[T: BaseModel]:
def __init__(self, session: AsyncSession, model_cls: type[T]) -> None:
self._session = session
self._model_cls = model_cls
async def get(self, id: str) -> T | None:
...
async def save(self, entity: T) -> T:
data = entity.model_dump()
...
return entity
class UserRepository(SQLRepository[User]):
async def find_by_email(self, email: str) -> User | None:
...
Self (3.11+) enables proper return typing for method chaining in inheritance hierarchies.
from typing import Self
from pydantic import BaseModel
class QueryBuilder[T]:
def __init__(self, model: type[T]) -> None:
self._model = model
self._filters: list[str] = []
self._order_by: str | None = None
self._limit: int | None = None
def filter(self, condition: str) -> Self:
self._filters.append(condition)
return self
def order_by(self, field: str) -> Self:
self._order_by = field
return self
def limit(self, n: int) -> Self:
self._limit = n
return self
def build(self) -> str:
query = f"SELECT * FROM {self._model.__name__}"
if self._filters:
query += " WHERE " + " AND ".join(self._filters)
if self._order_by:
query += f" ORDER BY {self._order_by}"
if self._limit:
query += f" LIMIT {self._limit}"
return query
Mark specific TypedDict fields as immutable for type checkers.
from typing import TypedDict, ReadOnly, Required, NotRequired
class Config(TypedDict):
host: ReadOnly[str] # Cannot be modified after creation
port: ReadOnly[int]
debug: NotRequired[bool] # Optional, but mutable if present
class APIResponse(TypedDict):
status: ReadOnly[Required[int]]
data: dict # Mutable
error: NotRequired[str]
def update_config(config: Config) -> None:
config["debug"] = True # OK — mutable field
config["host"] = "new.host" # Type error — ReadOnly
Type-safe event bus using generics.
import asyncio
from typing import Callable, Awaitable
from pydantic import BaseModel
class Event(BaseModel):
timestamp: float
class UserCreatedEvent(Event):
user_id: int
email: str
class OrderPlacedEvent(Event):
order_id: int
user_id: int
total: float
class TypedEventBus:
def __init__(self) -> None:
self._handlers: dict[type, list[Callable]] = {}
def subscribe[E: Event](
self,
event_type: type[E],
handler: Callable[[E], Awaitable[None]],
) -> None:
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
async def publish[E: Event](self, event: E) -> None:
handlers = self._handlers.get(type(event), [])
await asyncio.gather(*[h(event) for h in handlers])
Combine type information with validation constraints (works great with Pydantic).
from typing import Annotated
from pydantic import BaseModel, Field
type UserId = Annotated[int, Field(gt=0)]
type Email = Annotated[str, Field(pattern=r"^[\w.-]+@[\w.-]+\.\w+quot;)]
type Age = Annotated[int, Field(ge=0, le=150)]
type NonEmpty = Annotated[str, Field(min_length=1)]
class CreateUserRequest(BaseModel):
name: NonEmpty
email: Email
age: Age
class UserResponse(BaseModel):
id: UserId
name: str
email: str
When you need named parameters on callable types, use Protocol.
from typing import Protocol
from collections.abc import Callable, Awaitable
# Simple callables — use Callable
type ProgressCallback = Callable[[int, int], None]
type AsyncHandler = Callable[..., Awaitable[dict]]
# Named parameters — use Protocol
class OnProgress(Protocol):
def __call__(
self,
current: int,
total: int,
*,
message: str = "",
) -> None: ...
def process_items(
items: list[str],
on_progress: OnProgress | None = None,
) -> list[str]:
results = []
for i, item in enumerate(items):
if on_progress:
on_progress(i, len(items), message=f"Processing {item}")
results.append(item.upper())
return results
from typing import TypeAlias
from collections.abc import Iterator
# JSON type (recursive)
type JsonPrimitive = str | int | float | bool | None
type JsonArray = list[JsonValue]
type JsonObject = dict[str, JsonValue]
type JsonValue = JsonPrimitive | JsonArray | JsonObject
def parse_json(text: str) -> JsonValue:
import json
return json.loads(text)
# Tree structure
class TreeNode[T]:
value: T
children: list[TreeNode[T]]
def __init__(self, value: T) -> None:
self.value = value
self.children = []
def traverse(self) -> Iterator[T]:
yield self.value
for child in self.children:
yield from child.traverse()
Mark deprecated APIs so type checkers emit warnings.
import warnings
@warnings.deprecated("Use process_v2() instead")
def process(data: str) -> str:
return process_v2(data)
def process_v2(data: str) -> str:
return data.upper()
# Type checker will warn when calling process()
result = process("hello") # Deprecated: Use process_v2() instead
# pyproject.toml
[tool.ty.environment]
python-version = "3.14"
[tool.ty.rules]
# Promote common issues to errors
unresolved-import = "error"
invalid-assignment = "error"
invalid-argument-type = "error"
invalid-return-type = "error"
# Useful warnings
unused-ignore-comment = "warn"
possibly-missing-import = "warn"
deprecated = "warn"
division-by-zero = "warn"
[tool.ty.analysis]
# Suppress known unresolvable third-party imports
allowed-unresolved-imports = ["_typeshed.**"]
# Per-path overrides for legacy code
[[tool.ty.overrides]]
include = ["src/legacy/**"]
rules = { unresolved-attribute = "ignore" }
Use ruff's ANN rules (flake8-annotations) to enforce annotation coverage, and UP rules (pyupgrade) to modernize syntax.
# pyproject.toml
[tool.ruff]
target-version = "py313"
line-length = 120
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"UP", # pyupgrade — modernize syntax (e.g. remove old typing imports)
"ANN", # flake8-annotations — enforce type annotations
"B", # bugbear
"SIM", # simplify
"TCH", # flake8-type-checking — move type-only imports behind TYPE_CHECKING
"RUF", # ruff-specific rules
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ANN"] # Don't require annotations in tests
[tool.ruff.lint.flake8-type-checking]
runtime-evaluated-base-classes = ["pydantic.BaseModel"]
# Type check
ty check src/
# Type check in watch mode (development)
ty check --watch
# Lint + format
ruff check src/
ruff format src/
# All three in CI
uv run ty check src/ && uv run ruff check src/
# Suppress a specific ty diagnostic
result = greet(42) # ty: ignore[invalid-argument-type]
# Suppress ruff
x = 1 # noqa: ANN001
ANN rules enforce this.T | None — Modern union syntax everywhere. Never Optional[T] or Union[X, Y].list[str], dict[str, int], tuple[int, ...]. Never import from typing.def f[T](x: T) -> T: and class C[T]: instead of TypeVar.TypeIs over TypeGuard — Proper bidirectional narrowing (3.13+).type statement for aliases — type Point = tuple[float, float] instead of TypeAlias.Iterable[str], Sequence[str], Mapping[str, int] from collections.abc.Any — Use specific types or generics. Any is acceptable for truly dynamic data or untyped third-party code.Annotated with Pydantic Field — Rich type aliases that carry validation constraints.