python-type-safety
/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, Pydantic models, ty check, ruff type rules, or any static type analysis in Python.
- Author
- ai-riksarkivet ↗
- Repository
- ra-mcp ↗
- Licence
- Apache-2.0
- Last repository change
- 21.10.2026
Assessment and security
- Files in this package
- 1
- Scripts
- 0
- HTML comments
- 0
- URLs in SKILL.md
- 3
No signals detected. Repository changed on 21.10.2026, without references to retired models or APIs.
Apache-2.0, otwarta: można używać i zmieniać, z zachowaniem licencji i informacji o zmianach.
What it does and when to use it
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, Pydantic models, ty check, ruff type rules, or any static type analysis in Python.
Original author description
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, Pydantic models, ty check, ruff type rules, or any static type analysis in Python.
How it works
- Toolchain
- Key Python Version Features
- Fundamental Patterns
- Advanced Patterns
- Configuration
- Best Practices
SKILL.md preview
---
name: python-type-safety
description: 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, Pydantic models, ty check, ruff type rules, or any static type analysis in Python.
---
# Python Type Safety (3.13+ / 3.14+)
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.
## Toolchain
| 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](https://docs.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.
## Key Python Version Features
### Python 3.12
- **PEP 695** — Type parameter syntax: `def first[T](items: list[T]) -> T:`
- **PEP 695** — `type` statement for aliases: `type Point = tuple[float, float]`
### Python 3.13
- **PEP 696** — TypeVar/ParamSpec/TypeVarTuple defaults: `class Box[T = int]:`
- **PEP 742** — `TypeIs` for proper bidirectional type narrowing
- **PEP 705** — `ReadOnly` for TypedDict items
- **PEP 702** — `warnings.deprecated()` decorator with type system support
### Python 3.14
- **PEP 649/749** — Deferred evaluation of annotations (no more `from __future__ import annotations`, no more forward reference strings)
- **PEP 758** — `except` and `except*` expressions may omit brackets
## Fundamental Patterns
### Pattern 1: Annotate All Public Signatures
Every public function, method, and class should have type annotations. Use native generics everywhere (no `typing.List`, `typing.Dict`, etc. — those are ancient history).
```python
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:
...
```
### Pattern 2: Type Narrowing with TypeIs (3.13+)
`TypeIs` provides proper bidirectional narrowing — when the guard returns `False`, the type checker narrows the else branch too (unlike the older `TypeGuard`).
```python
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.
### Pattern 3: Modern Generic Classes (3.12+ Syntax)
Use the built-in type parameter syntax — no `TypeVar` boilerplate needed.
```python
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)))
```
### Pattern 4: TypeVar Defaults (3.13+)
Generic parameters can now have defaults, reducing boilerplate for common cases.
```python
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)
```
### Pattern 5: Protocols for Structural Typing
Define interfaces without inheritance — duck typing with full type safety.
```python
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
```
### Pattern 6: Deferred Annotations (3.14+)
In Python 3.14, annotations are evaluated lazily. Forward references just work — no string quoting, no `from __future__ import annotations`.
```python
# 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).
## Advanced Patterns
### Pattern 7: Discriminated Unions
Use `Literal` types for tagged unions with exhaustive pattern matching.
```python
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)
```
### Pattern 8: ParamSpec for Type-Safe Decorators
Preserve function signatures through decorators.
```python
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.
### Pattern 9: Conditional Types with Overloads
Return different types based on argument values.
```python
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")
```
### Pattern 10: Generic Repository with Pydantic
Type-safe data access using Pydantic models.
```python
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:
...
```
### Pattern 11: Builder Pattern with Self
`Self` (3.11+) enables proper return typing for method chaining in inheritance hierarchies.
```python
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
```
### Pattern 12: ReadOnly TypedDict (3.13+)
Mark specific TypedDict fields as immutable for type checkers.
```python
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
```
### Pattern 13: Typed Event System
Type-safe event bus using generics.
```python
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])
```
### Pattern 14: Annotated for Rich Metadata
Combine type information with validation constraints (works great with Pydantic).
```python
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+$")]
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
```
### Pattern 15: Callable Types with Protocol
When you need named parameters on callable types, use Protocol.
```python
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
```
### Pattern 16: Recursive Types
```python
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()
```
### Pattern 17: Deprecation Markers (3.13+)
Mark deprecated APIs so type checkers emit warnings.
```python
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
```
## Configuration
### ty Configuration
```toml
# 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" }
```
### ruff Configuration (Annotation Enforcement)
Use ruff's `ANN` rules (flake8-annotations) to enforce annotation coverage, and `UP` rules (pyupgrade) to modernize syntax.
```toml
# 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"]
```
### Running in CI
```bash
# 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/
```
### Inline Suppression
```python
# Suppress a specific ty diagnostic
result = greet(42) # ty: ignore[invalid-argument-type]
# Suppress ruff
x = 1 # noqa: ANN001
```
## Best Practices
1. **Use Pydantic for data models** — Never plain dataclasses. Pydantic gives you runtime validation, serialization, and schema generation alongside type safety.
2. **Annotate all public APIs** — Functions, methods, class attributes. Let ruff's `ANN` rules enforce this.
3. **Use `T | None`** — Modern union syntax everywhere. Never `Optional[T]` or `Union[X, Y]`.
4. **Use native generics** — `list[str]`, `dict[str, int]`, `tuple[int, ...]`. Never import from `typing`.
5. **Use 3.12+ type parameter syntax** — `def f[T](x: T) -> T:` and `class C[T]:` instead of `TypeVar`.
6. **Use `TypeIs` over `TypeGuard`** — Proper bidirectional narrowing (3.13+).
7. **Use `type` statement for aliases** — `type Point = tuple[float, float]` instead of `TypeAlias`.
8. **Prefer Protocols over ABCs** — Structural typing is more flexible than nominal typing.
9. **Use abstract collection types for parameters** — `Iterable[str]`, `Sequence[str]`, `Mapping[str, int]` from `collections.abc`.
10. **Minimize `Any`** — Use specific types or generics. `Any` is acceptable for truly dynamic data or untyped third-party code.
11. **Use `Annotated` with Pydantic `Field`** — Rich type aliases that carry validation constraints.
12. **Run ty + ruff in CI** — ty for type checking, ruff for annotation enforcement and modernization.
Versions
| Repository | Score | Licence |
|---|---|---|
| This page · ai-riksarkivet/ra-mcp | 95 | Apache-2.0 |
| wshobson/agents | 100 | MIT |
| wesleyegberto/software-engineering-skills | 100 | MIT |
Database snapshot: 13.11.2026 · package version 8c05cec1cc9f (SHA-256 digest of the entire package)
Downloads in the last 30 days: 0 · ranking
Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.
Similar skills
- 100python-typing-ops
Python type hints and type safety patterns. Triggers on: type hints, typing, TypeVar, Generic, Protocol, mypy, pyright, type annotation, overload, TypedDict. - 100python-code-quality
Python code quality with ruff and ty. Use when the user mentions ruff, ty, linting, formatting, type checking, or Python code style. - 95python-type-system
Use when Python's type system including type hints, mypy, Protocol, TypedDict, and Generics. Use when working with Python type annotations. - 95improving-python-code-quality
Improves Python library code quality through ruff linting, mypy type checking, Pythonic idioms, and refactoring. Use when reviewing code for quality issues, adding type hints, configuring static analysis tools, or refactoring Python library code. - 100python-quality
Python static analysis with ruff, mypy, pyright. Type checking and linting best practices. USE WHEN: "type checking", "mypy", "pyright", "ruff", "linting", "static analysis", "type error", "type hints", "pylint", "flake8", "black", "isort", "code quality" DO NOT USE FOR: Basic… - 100python-idioms
Modern Python (3.11+) idioms: type annotations, typing Protocols, Pydantic models, asyncio, pytest fixtures, and Ruff/Mypy strict compliance. Use when writing, refactoring, or reviewing Python applications, APIs, or scripts.