
Python Typing Patterns
- 1 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
python-typing-patterns is a skill that provides modern Python type-hint patterns and mypy/pyright configuration for type-safe code.
About
This skill covers Python type hints and type-safety patterns for Python 3.10+. It includes annotations, collections, Union and Optional, TypedDict, Callable, Generics, Protocols, type guards, and Literal or Final. A developer uses it to add type safety and to configure mypy or pyright.
- Modern Python type hints for 3.10+ union syntax
- TypedDict, Protocol, Generics, TypeGuard, Literal, Final
- mypy and pyright commands and config
Python Typing Patterns by the numbers
- 1 all-time installs (skills.sh)
- Ranked #240 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
python-typing-patterns capabilities & compatibility
- Capabilities
- refactoring · code review
- Use cases
- refactoring · code review
- Pricing
- Free
What python-typing-patterns says it does
Python type hints and type safety patterns. Triggers on: type hints, typing, TypeVar, Generic, Protocol, mypy, pyright, type annotation, overload, TypedDict.
Modern type hints for safe, documented Python code.
npx skills add https://github.com/aiskillstore/marketplace --skill python-typing-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Add modern Python type hints and configure mypy or pyright for type-safe code.
Who is it for?
Python developers adding type hints or configuring static type checkers.
Skip if: Runtime schema validation beyond type hints, or non-Python languages.
When should I use this skill?
Adding type annotations, using TypeVar/Protocol/TypedDict, or running mypy or pyright.
What you get
Produces type-safe Python code with TypedDict, Protocol, Generics, and a mypy/pyright config.
- typed Python code
- mypy/pyright config
By the numbers
- 9-row typing quick-reference table
- 6 reference docs plus a pyproject typing config asset
Files
Python Typing Patterns
Modern type hints for safe, documented Python code.
Basic Annotations
# Variables
name: str = "Alice"
count: int = 42
items: list[str] = ["a", "b"]
mapping: dict[str, int] = {"key": 1}
# Function signatures
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}!" * times
# None handling
def find(id: int) -> str | None:
return db.get(id) # May return NoneCollections
from collections.abc import Sequence, Mapping, Iterable
# Use collection ABCs for flexibility
def process(items: Sequence[str]) -> list[str]:
"""Accepts list, tuple, or any sequence."""
return [item.upper() for item in items]
def lookup(data: Mapping[str, int], key: str) -> int:
"""Accepts dict or any mapping."""
return data.get(key, 0)
# Nested types
Matrix = list[list[float]]
Config = dict[str, str | int | bool]Optional and Union
# Modern syntax (3.10+)
def find(id: int) -> User | None:
pass
def parse(value: str | int | float) -> str:
pass
# With default None
def fetch(url: str, timeout: float | None = None) -> bytes:
passTypedDict
from typing import TypedDict, Required, NotRequired
class UserDict(TypedDict):
id: int
name: str
email: str | None
class ConfigDict(TypedDict, total=False): # All optional
debug: bool
log_level: str
class APIResponse(TypedDict):
data: Required[list[dict]]
error: NotRequired[str]
def process_user(user: UserDict) -> str:
return user["name"] # Type-safe key accessCallable
from collections.abc import Callable
# Function type
Handler = Callable[[str, int], bool]
def register(callback: Callable[[str], None]) -> None:
pass
# With keyword args (use Protocol instead)
from typing import Protocol
class Processor(Protocol):
def __call__(self, data: str, *, verbose: bool = False) -> int:
...Generics
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else None
# Bounded TypeVar
from typing import SupportsFloat
N = TypeVar("N", bound=SupportsFloat)
def average(values: list[N]) -> float:
return sum(float(v) for v in values) / len(values)Protocol (Structural Typing)
from typing import Protocol
class Readable(Protocol):
def read(self, n: int = -1) -> bytes:
...
def load(source: Readable) -> dict:
"""Accepts any object with read() method."""
data = source.read()
return json.loads(data)
# Works with file, BytesIO, custom classes
load(open("data.json", "rb"))
load(io.BytesIO(b"{}"))Type Guards
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))Literal and Final
from typing import Literal, Final
Mode = Literal["read", "write", "append"]
def open_file(path: str, mode: Mode) -> None:
pass
# Constants
MAX_SIZE: Final = 1024
API_VERSION: Final[str] = "v2"Quick Reference
| Type | Use Case |
|---|---|
| `X \ | None` |
list[T] | Homogeneous list |
dict[K, V] | Dictionary |
Callable[[Args], Ret] | Function type |
TypeVar("T") | Generic parameter |
Protocol | Structural typing |
TypedDict | Dict with fixed keys |
Literal["a", "b"] | Specific values only |
Final | Cannot be reassigned |
Type Checker Commands
# mypy
mypy src/ --strict
# pyright
pyright src/
# In pyproject.toml
[tool.mypy]
strict = true
python_version = "3.11"Additional Resources
./references/generics-advanced.md- TypeVar, ParamSpec, TypeVarTuple./references/protocols-patterns.md- Structural typing, runtime protocols./references/type-narrowing.md- Guards, isinstance, assert./references/mypy-config.md- mypy/pyright configuration./references/runtime-validation.md- Pydantic v2, typeguard, beartype./references/overloads.md- @overload decorator patterns
Scripts
./scripts/check-types.sh- Run type checkers with common options
Assets
./assets/pyproject-typing.toml- Recommended mypy/pyright config
---
See Also
This is a foundation skill with no prerequisites.
Related Skills:
python-pytest-patterns- Type-safe fixtures and mocking
Build on this skill:
python-async-patterns- Async type annotationspython-fastapi-patterns- Pydantic models and validationpython-database-patterns- SQLAlchemy type annotations
# pyproject.toml - Type checker configuration
# Copy these sections to your pyproject.toml
# ============================================================
# mypy Configuration
# ============================================================
[tool.mypy]
# Python version to target
python_version = "3.11"
# Enable strict mode (recommended for new projects)
strict = true
# Additional strictness
warn_return_any = true
warn_unused_ignores = true
warn_unreachable = true
# Error reporting
show_error_codes = true
show_error_context = true
show_column_numbers = true
pretty = true
# Paths
files = ["src", "tests"]
exclude = [
"migrations/",
"venv/",
".venv/",
"__pycache__/",
"build/",
"dist/",
]
# Plugin support (uncomment as needed)
# plugins = [
# "pydantic.mypy",
# "sqlalchemy.ext.mypy.plugin",
# ]
# ============================================================
# Per-module overrides
# ============================================================
# Relax strictness for tests
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
disallow_untyped_calls = false
# Ignore missing stubs for common libraries
[[tool.mypy.overrides]]
module = [
"requests.*",
"boto3.*",
"botocore.*",
"celery.*",
"redis.*",
]
ignore_missing_imports = true
# Legacy code - gradually add types
# [[tool.mypy.overrides]]
# module = "legacy.*"
# ignore_errors = true
# ============================================================
# pyright Configuration
# ============================================================
[tool.pyright]
# Python version
pythonVersion = "3.11"
# Paths
include = ["src"]
exclude = [
"**/node_modules",
"**/__pycache__",
"venv",
".venv",
"build",
"dist",
]
# Type checking mode: off, basic, standard, strict
typeCheckingMode = "strict"
# Report settings (strict mode enables all by default)
reportMissingTypeStubs = false
reportUnusedImport = "warning"
reportUnusedVariable = "warning"
reportUnusedFunction = "warning"
# Useful additional checks
reportUninitializedInstanceVariable = true
reportIncompatibleMethodOverride = true
reportIncompatibleVariableOverride = true
# ============================================================
# Recommended dev dependencies
# ============================================================
# [project.optional-dependencies]
# dev = [
# "mypy>=1.8.0",
# "pyright>=1.1.350",
# # Common type stubs
# "types-requests",
# "types-redis",
# "types-PyYAML",
# "types-python-dateutil",
# ]
Advanced Generics
Deep dive into Python's generic type system.
TypeVar Basics
from typing import TypeVar
# Unconstrained TypeVar
T = TypeVar("T")
def identity(x: T) -> T:
return x
# Usage - type is preserved
reveal_type(identity(42)) # int
reveal_type(identity("hello")) # strBounded TypeVar
from typing import TypeVar
# Upper bound - T must be subtype of bound
class Animal:
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "woof"
A = TypeVar("A", bound=Animal)
def make_speak(animal: A) -> A:
print(animal.speak())
return animal
# Works with Animal or any subclass
dog = make_speak(Dog()) # Returns Dog, not AnimalConstrained TypeVar
from typing import TypeVar
# Constrained to specific types
StrOrBytes = TypeVar("StrOrBytes", str, bytes)
def concat(a: StrOrBytes, b: StrOrBytes) -> StrOrBytes:
return a + b
# Must be same type
concat("a", "b") # OK -> str
concat(b"a", b"b") # OK -> bytes
# concat("a", b"b") # Error: can't mixGeneric 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()
def peek(self) -> T | None:
return self._items[-1] if self._items else None
# Usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value = int_stack.pop() # int
str_stack: Stack[str] = Stack()
str_stack.push("hello")Multiple Type Parameters
from typing import Generic, TypeVar
K = TypeVar("K")
V = TypeVar("V")
class Pair(Generic[K, V]):
def __init__(self, key: K, value: V) -> None:
self.key = key
self.value = value
def swap(self) -> "Pair[V, K]":
return Pair(self.value, self.key)
pair: Pair[str, int] = Pair("age", 30)
swapped = pair.swap() # Pair[int, str]Self Type (Python 3.11+)
from typing import Self
class Builder:
def __init__(self) -> None:
self.value = ""
def add(self, text: str) -> Self:
self.value += text
return self
def build(self) -> str:
return self.value
class HTMLBuilder(Builder):
def tag(self, name: str) -> Self:
self.value = f"<{name}>{self.value}</{name}>"
return self
# Chaining works with correct types
html = HTMLBuilder().add("Hello").tag("p").build()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]:
"""Decorator that preserves function signature."""
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@with_logging
def greet(name: str, excited: bool = False) -> str:
return f"Hello, {name}{'!' if excited else '.'}"
# Signature preserved:
greet("Alice", excited=True) # OK
# greet(123) # Type errorTypeVarTuple (Python 3.11+)
from typing import TypeVarTuple, Unpack
Ts = TypeVarTuple("Ts")
def concat_tuples(
a: tuple[*Ts],
b: tuple[*Ts]
) -> tuple[*Ts, *Ts]:
return (*a, *b)
# Usage
result = concat_tuples((1, "a"), (2, "b"))
# result: tuple[int, str, int, str]Covariance and Contravariance
from typing import TypeVar
# Covariant: Can use subtype
T_co = TypeVar("T_co", covariant=True)
class Reader(Generic[T_co]):
def read(self) -> T_co:
...
# Contravariant: Can use supertype
T_contra = TypeVar("T_contra", contravariant=True)
class Writer(Generic[T_contra]):
def write(self, value: T_contra) -> None:
...
# Invariant (default): Must be exact type
T = TypeVar("T") # Invariant
class Container(Generic[T]):
def get(self) -> T:
...
def set(self, value: T) -> None:
...Generic Protocols
from typing import Protocol, TypeVar
T = TypeVar("T")
class Comparable(Protocol[T]):
def __lt__(self, other: T) -> bool:
...
def __gt__(self, other: T) -> bool:
...
def max_value(a: T, b: T) -> T:
return a if a > b else b
# Works with any comparable type
max_value(1, 2) # int
max_value("a", "b") # strType Aliases
from typing import TypeAlias
# Simple alias
Vector: TypeAlias = list[float]
Matrix: TypeAlias = list[Vector]
# Generic alias
from typing import TypeVar
T = TypeVar("T")
Result: TypeAlias = tuple[T, str | None]
def parse(data: str) -> Result[int]:
try:
return (int(data), None)
except ValueError as e:
return (0, str(e))NewType
from typing import NewType
# Create distinct types for type safety
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def get_user(user_id: UserId) -> dict:
...
def get_order(order_id: OrderId) -> dict:
...
user_id = UserId(42)
order_id = OrderId(42)
get_user(user_id) # OK
# get_user(order_id) # Type error!
# get_user(42) # Type error!Best Practices
1. Name TypeVars descriptively - T, K, V for simple cases; ItemT, KeyT for complex 2. Use bounds - When you need method access on type parameter 3. Prefer Protocol - Over ABC for structural typing 4. Use Self - Instead of quoted class names in return types 5. Covariance - For read-only containers 6. Contravariance - For write-only/function parameter types 7. Invariance - For mutable containers (default, usually correct)
mypy and pyright Configuration
Type checker setup for strict, practical type safety.
mypy Configuration
pyproject.toml (Recommended)
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true
show_error_codes = true
show_error_context = true
# Paths
files = ["src", "tests"]
exclude = [
"migrations/",
"venv/",
"__pycache__/",
]
# Per-module overrides
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = [
"requests.*",
"boto3.*",
"botocore.*",
]
ignore_missing_imports = truemypy.ini (Alternative)
[mypy]
python_version = 3.11
strict = True
warn_return_any = True
warn_unused_ignores = True
show_error_codes = True
[mypy-tests.*]
disallow_untyped_defs = False
[mypy-requests.*]
ignore_missing_imports = Truemypy Flags Explained
Strict Mode Components
[tool.mypy]
# strict = true enables all of these:
warn_unused_configs = true
disallow_any_generics = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
no_implicit_reexport = true
strict_equality = true
extra_checks = trueCommonly Adjusted Flags
[tool.mypy]
# Allow untyped defs in some files
disallow_untyped_defs = true
# But not for tests
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
# Ignore third-party stubs
ignore_missing_imports = true # Global fallback
# Show where errors occur
show_error_context = true
show_column_numbers = true
show_error_codes = true
# Error output format
pretty = truepyright Configuration
pyrightconfig.json
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__", "venv"],
"pythonVersion": "3.11",
"pythonPlatform": "All",
"typeCheckingMode": "strict",
"reportMissingImports": true,
"reportMissingTypeStubs": false,
"reportUnusedImport": true,
"reportUnusedClass": true,
"reportUnusedFunction": true,
"reportUnusedVariable": true,
"reportDuplicateImport": true,
"reportPrivateUsage": true,
"reportConstantRedefinition": true,
"reportIncompatibleMethodOverride": true,
"reportIncompatibleVariableOverride": true,
"reportInconsistentConstructor": true,
"reportOverlappingOverload": true,
"reportUninitializedInstanceVariable": true
}pyproject.toml (pyright)
[tool.pyright]
include = ["src"]
exclude = ["**/node_modules", "**/__pycache__", "venv"]
pythonVersion = "3.11"
typeCheckingMode = "strict"
reportMissingTypeStubs = falseType Checking Modes
pyright Modes
{
"typeCheckingMode": "off" // No checking
"typeCheckingMode": "basic" // Basic checks
"typeCheckingMode": "standard" // Standard checks
"typeCheckingMode": "strict" // All checks enabled
}Inline Type Ignores
# Ignore specific error
result = some_call() # type: ignore[arg-type]
# Ignore all errors on line
result = some_call() # type: ignore
# With mypy error code
value = data["key"] # type: ignore[typeddict-item]
# With pyright
result = func() # pyright: ignore[reportGeneralTypeIssues]Type Stub Files (.pyi)
# mymodule.pyi - Type stubs for mymodule.py
def process(data: dict[str, int]) -> list[int]: ...
class Handler:
def __init__(self, name: str) -> None: ...
def handle(self, event: Event) -> bool: ...Stub Package Structure
stubs/
├── mypackage/
│ ├── __init__.pyi
│ ├── module.pyi
│ └── subpackage/
│ └── __init__.pyi[tool.mypy]
mypy_path = "stubs"CI Integration
GitHub Actions
name: Type Check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install mypy
pip install -e .[dev]
- name: Run mypy
run: mypy src/
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install -e .[dev]
- name: Run pyright
uses: jakebailey/pyright-action@v2Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-requests]
args: [--strict]Common Type Stubs
# Install type stubs
pip install types-requests
pip install types-redis
pip install types-PyYAML
pip install boto3-stubs[essential]
# Or use mypy to find missing stubs
mypy --install-types src/Gradual Typing Strategy
Phase 1: Basic
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_ignores = truePhase 2: Stricter
[tool.mypy]
python_version = "3.11"
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = truePhase 3: Strict
[tool.mypy]
python_version = "3.11"
strict = true
# Temporarily ignore problem areas
[[tool.mypy.overrides]]
module = "legacy.*"
ignore_errors = trueQuick Reference
| mypy Flag | Description |
|---|---|
--strict | Enable all strict checks |
--show-error-codes | Show error codes for ignores |
--ignore-missing-imports | Skip untyped libraries |
--python-version 3.11 | Target Python version |
--install-types | Install missing stubs |
--config-file | Specify config file |
| pyright Mode | Description |
|---|---|
off | No checking |
basic | Minimal checks |
standard | Recommended |
strict | All checks |
Function Overloads
Type-safe function signatures with @overload.
Basic Overloads
from typing import overload, Literal
# Overload signatures (no implementation)
@overload
def process(data: str) -> str: ...
@overload
def process(data: bytes) -> bytes: ...
@overload
def process(data: int) -> int: ...
# Actual implementation
def process(data: str | bytes | int) -> str | bytes | int:
if isinstance(data, str):
return data.upper()
elif isinstance(data, bytes):
return data.upper()
else:
return data * 2
# Type checker knows the return type
result = process("hello") # str
result = process(b"hello") # bytes
result = process(42) # intOverloads with Literal
from typing import overload, Literal
@overload
def fetch(url: str, format: Literal["json"]) -> dict: ...
@overload
def fetch(url: str, format: Literal["text"]) -> str: ...
@overload
def fetch(url: str, format: Literal["bytes"]) -> bytes: ...
def fetch(url: str, format: str) -> dict | str | bytes:
response = requests.get(url)
if format == "json":
return response.json()
elif format == "text":
return response.text
else:
return response.content
# Usage - return type is known
data = fetch("https://api.example.com", "json") # dict
text = fetch("https://api.example.com", "text") # strOverloads with Optional Parameters
from typing import overload
@overload
def get_user(user_id: int) -> User: ...
@overload
def get_user(user_id: int, include_posts: Literal[True]) -> UserWithPosts: ...
@overload
def get_user(user_id: int, include_posts: Literal[False]) -> User: ...
def get_user(user_id: int, include_posts: bool = False) -> User | UserWithPosts:
user = db.get_user(user_id)
if include_posts:
user.posts = db.get_posts(user_id)
return UserWithPosts(**user.__dict__)
return user
# Type-safe usage
user = get_user(1) # User
user_with_posts = get_user(1, include_posts=True) # UserWithPostsOverloads with None Returns
from typing import overload
@overload
def find(items: list[T], predicate: Callable[[T], bool]) -> T | None: ...
@overload
def find(items: list[T], predicate: Callable[[T], bool], default: T) -> T: ...
def find(
items: list[T],
predicate: Callable[[T], bool],
default: T | None = None
) -> T | None:
for item in items:
if predicate(item):
return item
return default
# Without default - might be None
result = find([1, 2, 3], lambda x: x > 5) # int | None
# With default - never None
result = find([1, 2, 3], lambda x: x > 5, default=0) # intClass Method Overloads
from typing import overload, Self
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@overload
@classmethod
def from_tuple(cls, coords: tuple[float, float]) -> Self: ...
@overload
@classmethod
def from_tuple(cls, coords: tuple[float, float, float]) -> "Point3D": ...
@classmethod
def from_tuple(cls, coords: tuple[float, ...]) -> "Point | Point3D":
if len(coords) == 2:
return cls(coords[0], coords[1])
elif len(coords) == 3:
return Point3D(coords[0], coords[1], coords[2])
raise ValueError("Expected 2 or 3 coordinates")Overloads with Generics
from typing import overload, TypeVar, Sequence
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
@overload
def first(items: Sequence[T]) -> T | None: ...
@overload
def first(items: Sequence[T], default: T) -> T: ...
def first(items: Sequence[T], default: T | None = None) -> T | None:
return items[0] if items else default
@overload
def get(d: dict[K, V], key: K) -> V | None: ...
@overload
def get(d: dict[K, V], key: K, default: V) -> V: ...
def get(d: dict[K, V], key: K, default: V | None = None) -> V | None:
return d.get(key, default)Async Overloads
from typing import overload
@overload
async def fetch_data(url: str, as_json: Literal[True]) -> dict: ...
@overload
async def fetch_data(url: str, as_json: Literal[False] = False) -> str: ...
async def fetch_data(url: str, as_json: bool = False) -> dict | str:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if as_json:
return await response.json()
return await response.text()Property Overloads (Getter/Setter)
from typing import overload
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius
@property
def value(self) -> float:
return self._celsius
@overload
def convert(self, unit: Literal["C"]) -> float: ...
@overload
def convert(self, unit: Literal["F"]) -> float: ...
@overload
def convert(self, unit: Literal["K"]) -> float: ...
def convert(self, unit: str) -> float:
if unit == "C":
return self._celsius
elif unit == "F":
return self._celsius * 9/5 + 32
elif unit == "K":
return self._celsius + 273.15
raise ValueError(f"Unknown unit: {unit}")Common Patterns
from typing import overload, Literal, TypeVar
T = TypeVar("T")
# Pattern 1: Return type based on flag
@overload
def parse(data: str, strict: Literal[True]) -> Result: ...
@overload
def parse(data: str, strict: Literal[False] = False) -> Result | None: ...
# Pattern 2: Different return for different input types
@overload
def normalize(value: str) -> str: ...
@overload
def normalize(value: list[str]) -> list[str]: ...
@overload
def normalize(value: dict[str, str]) -> dict[str, str]: ...
# Pattern 3: Optional vs required parameter
@overload
def create(name: str) -> Item: ...
@overload
def create(name: str, *, template: str) -> Item: ...Quick Reference
| Pattern | Use Case |
|---|---|
@overload | Define signature (no body) |
Literal["value"] | Specific string/int values |
| `T \ | None vs T` |
| Implementation | Must handle all overload cases |
| Rule | Description |
|---|---|
| No body in overloads | Use ... (ellipsis) |
| Implementation last | After all overloads |
| Cover all cases | Implementation must accept all overload inputs |
| Static only | Overloads are for type checkers, not runtime |
Protocol Patterns
Structural typing with Protocol for flexible, decoupled code.
Basic Protocol
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render(shape: Drawable) -> None:
shape.draw()
# Both work - no inheritance needed
render(Circle())
render(Square())Protocol with Attributes
from typing import Protocol
class Named(Protocol):
name: str
class HasId(Protocol):
id: int
name: str
class User:
def __init__(self, id: int, name: str):
self.id = id
self.name = name
def greet(entity: Named) -> str:
return f"Hello, {entity.name}"
# Works with any object having 'name' attribute
greet(User(1, "Alice"))Protocol with Methods
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None:
...
class Flushable(Protocol):
def flush(self) -> None:
...
class CloseableAndFlushable(Closeable, Flushable, Protocol):
"""Combined protocol."""
pass
def cleanup(resource: CloseableAndFlushable) -> None:
resource.flush()
resource.close()Callable Protocol
from typing import Protocol
class Comparator(Protocol):
def __call__(self, a: int, b: int) -> int:
"""Return negative, zero, or positive."""
...
def sort_with(items: list[int], cmp: Comparator) -> list[int]:
return sorted(items, key=lambda x: cmp(x, 0))
# Lambda works
sort_with([3, 1, 2], lambda a, b: a - b)
# Function works
def compare(a: int, b: int) -> int:
return a - b
sort_with([3, 1, 2], compare)Generic Protocol
from typing import Protocol, TypeVar
T = TypeVar("T")
class Container(Protocol[T]):
def get(self) -> T:
...
def set(self, value: T) -> None:
...
class Box:
def __init__(self, value: int):
self._value = value
def get(self) -> int:
return self._value
def set(self, value: int) -> None:
self._value = value
def process(container: Container[int]) -> int:
value = container.get()
container.set(value * 2)
return container.get()
process(Box(5)) # Returns 10Runtime Checkable Protocol
from typing import Protocol, runtime_checkable
@runtime_checkable
class Sized(Protocol):
def __len__(self) -> int:
...
# Now isinstance() works
def process(obj: object) -> int:
if isinstance(obj, Sized):
return len(obj)
return 0
process([1, 2, 3]) # 3
process("hello") # 5
process(42) # 0Protocol vs ABC
from abc import ABC, abstractmethod
from typing import Protocol
# ABC - Requires explicit inheritance
class AbstractReader(ABC):
@abstractmethod
def read(self) -> str:
pass
class FileReader(AbstractReader): # Must inherit
def read(self) -> str:
return "content"
# Protocol - Structural (duck typing)
class ReaderProtocol(Protocol):
def read(self) -> str:
...
class AnyReader: # No inheritance needed
def read(self) -> str:
return "content"
def process(reader: ReaderProtocol) -> str:
return reader.read()
process(AnyReader()) # Works!
process(FileReader()) # Also works!Common Protocols
Supports Protocols
from typing import SupportsInt, SupportsFloat, SupportsBytes, SupportsAbs
def to_int(value: SupportsInt) -> int:
return int(value)
to_int(3.14) # OK - float supports __int__
to_int("42") # Error - str doesn't support __int__Iterator Protocol
from typing import Protocol, TypeVar
T = TypeVar("T", covariant=True)
class Iterator(Protocol[T]):
def __next__(self) -> T:
...
class Iterable(Protocol[T]):
def __iter__(self) -> Iterator[T]:
...Context Manager Protocol
from typing import Protocol, TypeVar
T = TypeVar("T")
class ContextManager(Protocol[T]):
def __enter__(self) -> T:
...
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: object | None,
) -> bool | None:
...Real-World Patterns
Repository Pattern
from typing import Protocol, TypeVar
T = TypeVar("T")
class Repository(Protocol[T]):
def get(self, id: int) -> T | None:
...
def save(self, entity: T) -> None:
...
def delete(self, id: int) -> bool:
...
class User:
id: int
name: str
class InMemoryUserRepo:
def __init__(self):
self._data: dict[int, User] = {}
def get(self, id: int) -> User | None:
return self._data.get(id)
def save(self, entity: User) -> None:
self._data[entity.id] = entity
def delete(self, id: int) -> bool:
return self._data.pop(id, None) is not None
def process_users(repo: Repository[User]) -> None:
user = repo.get(1)
if user:
repo.delete(user.id)Event Handler
from typing import Protocol
class Event:
pass
class UserCreated(Event):
def __init__(self, user_id: int):
self.user_id = user_id
class EventHandler(Protocol):
def can_handle(self, event: Event) -> bool:
...
def handle(self, event: Event) -> None:
...
class UserCreatedHandler:
def can_handle(self, event: Event) -> bool:
return isinstance(event, UserCreated)
def handle(self, event: Event) -> None:
if isinstance(event, UserCreated):
print(f"User {event.user_id} created")
def dispatch(event: Event, handlers: list[EventHandler]) -> None:
for handler in handlers:
if handler.can_handle(event):
handler.handle(event)Best Practices
1. Prefer Protocol over ABC - For external interfaces 2. Use @runtime_checkable sparingly - Has performance cost 3. Keep protocols minimal - Single responsibility 4. Document expected behavior - Protocols only define shape, not behavior 5. Combine protocols - For complex requirements 6. Use Generic protocols - For type-safe containers
Runtime Type Validation
Enforce type hints at runtime with Pydantic, typeguard, and beartype.
Pydantic v2 Validation
from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import EmailStr, HttpUrl, PositiveInt
from datetime import datetime
from typing import Self
class User(BaseModel):
"""Model with automatic validation."""
id: PositiveInt
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
website: HttpUrl | None = None
created_at: datetime = Field(default_factory=datetime.now)
@field_validator("name")
@classmethod
def name_must_be_title_case(cls, v: str) -> str:
return v.title()
@model_validator(mode="after")
def check_consistency(self) -> Self:
# Cross-field validation
return self
# Usage - raises ValidationError on invalid data
user = User(id=1, name="john doe", email="john@example.com")
print(user.name) # "John Doe" (transformed)
# From dict
user = User.model_validate({"id": 1, "name": "jane", "email": "jane@example.com"})
# Validation error
try:
User(id=-1, name="", email="invalid")
except ValidationError as e:
print(e.errors())Pydantic for Function Arguments
from pydantic import validate_call, Field
from typing import Annotated
@validate_call
def greet(
name: Annotated[str, Field(min_length=1)],
count: Annotated[int, Field(ge=1, le=10)] = 1,
) -> str:
return f"Hello, {name}!" * count
# Valid
greet("World") # OK
greet("World", count=3) # OK
# Invalid - raises ValidationError
greet("") # Error: min_length
greet("World", count=100) # Error: letypeguard (Runtime Type Checking)
from typeguard import typechecked, check_type
from typing import TypeVar, Generic
# Decorator for function checking
@typechecked
def process(items: list[int], multiplier: float) -> list[float]:
return [item * multiplier for item in items]
# Valid
process([1, 2, 3], 1.5) # OK
# Invalid - raises TypeCheckError at runtime
process(["a", "b"], 1.5) # Error: list[int] expected
# Check types manually
from typeguard import check_type
value = [1, 2, 3]
check_type(value, list[int]) # OK
value = [1, "two", 3]
check_type(value, list[int]) # TypeCheckError
# Class checking
@typechecked
class DataProcessor(Generic[T]):
def __init__(self, data: list[T]):
self.data = data
def process(self) -> T:
return self.data[0]beartype (Fast Runtime Checking)
from beartype import beartype
from beartype.typing import List, Optional
# ~200x faster than typeguard
@beartype
def fast_process(items: List[int], factor: float) -> List[float]:
return [i * factor for i in items]
# With optional
@beartype
def find_user(user_id: int) -> Optional[dict]:
return None
# Class decorator
@beartype
class FastProcessor:
def __init__(self, data: list[int]):
self.data = data
def sum(self) -> int:
return sum(self.data)TypedDict Runtime Validation
from typing import TypedDict, Required, NotRequired
from pydantic import TypeAdapter
class UserDict(TypedDict):
id: Required[int]
name: Required[str]
email: NotRequired[str]
# Using Pydantic to validate TypedDict
adapter = TypeAdapter(UserDict)
# Valid
user = adapter.validate_python({"id": 1, "name": "John"})
# Invalid - raises ValidationError
adapter.validate_python({"id": "not-int", "name": "John"})
# JSON parsing with validation
user = adapter.validate_json('{"id": 1, "name": "John"}')dataclass Validation with Pydantic
from dataclasses import dataclass
from pydantic import TypeAdapter
from typing import Annotated
from annotated_types import Gt, Lt
@dataclass
class Point:
x: Annotated[float, Gt(-100), Lt(100)]
y: Annotated[float, Gt(-100), Lt(100)]
# Create validator
validator = TypeAdapter(Point)
# Validate
point = validator.validate_python({"x": 10.5, "y": 20.3})
# Or with init
point = validator.validate_python(Point(x=10.5, y=20.3))Custom Validators
from pydantic import BaseModel, field_validator, ValidationInfo
from pydantic_core import PydanticCustomError
import re
class Account(BaseModel):
username: str
password: str
@field_validator("username")
@classmethod
def validate_username(cls, v: str) -> str:
if not re.match(r"^[a-z][a-z0-9_]{2,19}$", v):
raise PydanticCustomError(
"invalid_username",
"Username must be 3-20 chars, start with letter, contain only a-z, 0-9, _"
)
return v
@field_validator("password")
@classmethod
def validate_password(cls, v: str, info: ValidationInfo) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if info.data.get("username") and info.data["username"] in v:
raise ValueError("Password cannot contain username")
return vConstrained Types
from pydantic import (
BaseModel,
PositiveInt,
NegativeFloat,
conint,
constr,
conlist,
)
class Order(BaseModel):
quantity: PositiveInt # > 0
discount: NegativeFloat | None = None # < 0
# Custom constraints
product_code: constr(pattern=r"^[A-Z]{3}-\d{4}$")
priority: conint(ge=1, le=5)
tags: conlist(str, min_length=1, max_length=10)
# Usage
order = Order(
quantity=5,
product_code="ABC-1234",
priority=3,
tags=["urgent"]
)When to Use Each
| Tool | Speed | Strictness | Use Case |
|---|---|---|---|
| Pydantic | Medium | High | API validation, config |
| typeguard | Slow | Very high | Testing, debugging |
| beartype | Fast | Medium | Production code |
# Development: Use typeguard for strictest checking
from typeguard import typechecked
@typechecked
def dev_function(x: list[int]) -> int:
return sum(x)
# Production: Use beartype for minimal overhead
from beartype import beartype
@beartype
def prod_function(x: list[int]) -> int:
return sum(x)
# API boundaries: Use Pydantic for validation + serialization
from pydantic import BaseModel
class Request(BaseModel):
items: list[int]
def api_function(request: Request) -> int:
return sum(request.items)Quick Reference
| Library | Decorator | Check |
|---|---|---|
| Pydantic | @validate_call | Model.model_validate() |
| typeguard | @typechecked | check_type(val, Type) |
| beartype | @beartype | Automatic on call |
| Pydantic Type | Constraint |
|---|---|
PositiveInt | > 0 |
NegativeInt | < 0 |
conint(ge=0, le=100) | 0 <= x <= 100 |
constr(min_length=1) | Non-empty string |
EmailStr | Valid email |
HttpUrl | Valid URL |
Type Narrowing
Techniques for narrowing types in conditional branches.
isinstance Narrowing
def process(value: str | int | list[str]) -> str:
if isinstance(value, str):
# value is str here
return value.upper()
elif isinstance(value, int):
# value is int here
return str(value * 2)
else:
# value is list[str] here
return ", ".join(value)None Checks
def greet(name: str | None) -> str:
if name is None:
return "Hello, stranger"
# name is str here (not None)
return f"Hello, {name}"
# Also works with truthiness
def greet_truthy(name: str | None) -> str:
if name:
# name is str here
return f"Hello, {name}"
return "Hello, stranger"Assertion Narrowing
def process(data: dict | None) -> str:
assert data is not None
# data is dict here
return str(data.get("key"))
def validate(value: int | str) -> int:
assert isinstance(value, int), "Must be int"
# value is int here
return value * 2Type Guards
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
"""Check if all elements are strings."""
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> str:
if is_string_list(items):
# items is list[str] here
return ", ".join(items)
return "Not all strings"
# With TypeVar
from typing import TypeVar
T = TypeVar("T")
def is_not_none(val: T | None) -> TypeGuard[T]:
return val is not None
def process_optional(value: str | None) -> str:
if is_not_none(value):
# value is str here
return value.upper()
return "default"TypeIs (Python 3.13+)
from typing import TypeIs
# TypeIs narrows more aggressively than TypeGuard
def is_str(val: object) -> TypeIs[str]:
return isinstance(val, str)
def process(value: object) -> str:
if is_str(value):
# value is str here
return value.upper()
return "not a string"Discriminated Unions
from typing import Literal, TypedDict
class SuccessResult(TypedDict):
status: Literal["success"]
data: dict
class ErrorResult(TypedDict):
status: Literal["error"]
message: str
Result = SuccessResult | ErrorResult
def handle_result(result: Result) -> str:
if result["status"] == "success":
# result is SuccessResult
return str(result["data"])
else:
# result is ErrorResult
return f"Error: {result['message']}"Match Statement (Python 3.10+)
def describe(value: int | str | list[int]) -> str:
match value:
case int(n):
return f"Integer: {n}"
case str(s):
return f"String: {s}"
case [first, *rest]:
return f"List starting with {first}"
case _:
return "Unknown"hasattr Narrowing
from typing import Protocol
class HasName(Protocol):
name: str
def greet(obj: object) -> str:
if hasattr(obj, "name") and isinstance(obj.name, str):
# Type checkers may not narrow here
# Use Protocol + isinstance instead
return f"Hello, {obj.name}"
return "Hello"Callable Narrowing
from collections.abc import Callable
def execute(func_or_value: Callable[[], int] | int) -> int:
if callable(func_or_value):
# func_or_value is Callable[[], int]
return func_or_value()
else:
# func_or_value is int
return func_or_valueExhaustiveness Checking
from typing import Literal, Never
def assert_never(value: Never) -> Never:
raise AssertionError(f"Unexpected value: {value}")
Status = Literal["pending", "active", "closed"]
def handle_status(status: Status) -> str:
if status == "pending":
return "Waiting..."
elif status == "active":
return "In progress"
elif status == "closed":
return "Done"
else:
# If we add a new status, type checker will error here
assert_never(status)Narrowing in Loops
from typing import TypeGuard
def is_valid(item: str | None) -> TypeGuard[str]:
return item is not None
def process_items(items: list[str | None]) -> list[str]:
result: list[str] = []
for item in items:
if is_valid(item):
# item is str here
result.append(item.upper())
return result
# Or use filter with type guard
def process_items_functional(items: list[str | None]) -> list[str]:
valid_items = filter(is_valid, items)
return [item.upper() for item in valid_items]Class Type Narrowing
class Animal:
pass
class Dog(Animal):
def bark(self) -> str:
return "Woof!"
class Cat(Animal):
def meow(self) -> str:
return "Meow!"
def make_sound(animal: Animal) -> str:
if isinstance(animal, Dog):
return animal.bark() # animal is Dog
elif isinstance(animal, Cat):
return animal.meow() # animal is Cat
return "..."Common Patterns
Optional Unwrapping
def unwrap_or_default(value: T | None, default: T) -> T:
if value is not None:
return value
return default
# With early return
def process(data: dict | None) -> dict:
if data is None:
return {}
# data is dict for rest of function
return {k: v.upper() for k, v in data.items()}Safe Dictionary Access
def get_nested(data: dict, *keys: str) -> object | None:
result: object = data
for key in keys:
if not isinstance(result, dict):
return None
result = result.get(key)
if result is None:
return None
return resultBest Practices
1. Prefer isinstance - Most reliable for type narrowing 2. Use TypeGuard - For complex conditions 3. Check None explicitly - is None or is not None 4. Use exhaustiveness checks - Catch missing cases 5. Avoid hasattr - Type checkers struggle with it 6. Match statements - Clean pattern matching (3.10+)
#!/bin/bash
# Run type checkers with common options
# Usage: ./check-types.sh [--mypy|--pyright|--both] [--strict] [path]
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Defaults
CHECKER="both"
STRICT=""
TARGET="src"
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--mypy)
CHECKER="mypy"
shift
;;
--pyright)
CHECKER="pyright"
shift
;;
--both)
CHECKER="both"
shift
;;
--strict)
STRICT="--strict"
shift
;;
*)
TARGET="$1"
shift
;;
esac
done
# Check if target exists
if [[ ! -e "$TARGET" ]]; then
echo -e "${RED}Target not found: $TARGET${NC}"
exit 1
fi
run_mypy() {
echo -e "${BLUE}=== Running mypy ===${NC}"
if ! command -v mypy &> /dev/null; then
echo -e "${YELLOW}mypy not found. Install with: pip install mypy${NC}"
return 1
fi
MYPY_ARGS="--show-error-codes --show-error-context --pretty"
if [[ -n "$STRICT" ]]; then
MYPY_ARGS="$MYPY_ARGS --strict"
fi
echo "mypy $MYPY_ARGS $TARGET"
echo ""
if mypy $MYPY_ARGS "$TARGET"; then
echo -e "${GREEN}✓ mypy passed${NC}"
return 0
else
echo -e "${RED}✗ mypy found errors${NC}"
return 1
fi
}
run_pyright() {
echo -e "${BLUE}=== Running pyright ===${NC}"
if ! command -v pyright &> /dev/null; then
echo -e "${YELLOW}pyright not found. Install with: pip install pyright${NC}"
return 1
fi
PYRIGHT_ARGS=""
if [[ -n "$STRICT" ]]; then
# Create temporary config for strict mode
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << EOF
{
"typeCheckingMode": "strict"
}
EOF
PYRIGHT_ARGS="--project $TEMP_CONFIG"
fi
echo "pyright $PYRIGHT_ARGS $TARGET"
echo ""
if pyright $PYRIGHT_ARGS "$TARGET"; then
echo -e "${GREEN}✓ pyright passed${NC}"
[[ -n "$STRICT" ]] && rm -f "$TEMP_CONFIG"
return 0
else
echo -e "${RED}✗ pyright found errors${NC}"
[[ -n "$STRICT" ]] && rm -f "$TEMP_CONFIG"
return 1
fi
}
# Run checkers
MYPY_STATUS=0
PYRIGHT_STATUS=0
case $CHECKER in
mypy)
run_mypy || MYPY_STATUS=$?
;;
pyright)
run_pyright || PYRIGHT_STATUS=$?
;;
both)
run_mypy || MYPY_STATUS=$?
echo ""
run_pyright || PYRIGHT_STATUS=$?
;;
esac
# Summary
echo ""
echo -e "${BLUE}=== Summary ===${NC}"
if [[ "$CHECKER" == "both" ]] || [[ "$CHECKER" == "mypy" ]]; then
if [[ $MYPY_STATUS -eq 0 ]]; then
echo -e "mypy: ${GREEN}✓ passed${NC}"
else
echo -e "mypy: ${RED}✗ failed${NC}"
fi
fi
if [[ "$CHECKER" == "both" ]] || [[ "$CHECKER" == "pyright" ]]; then
if [[ $PYRIGHT_STATUS -eq 0 ]]; then
echo -e "pyright: ${GREEN}✓ passed${NC}"
else
echo -e "pyright: ${RED}✗ failed${NC}"
fi
fi
# Exit with error if any checker failed
if [[ $MYPY_STATUS -ne 0 ]] || [[ $PYRIGHT_STATUS -ne 0 ]]; then
exit 1
fi
{
"schema_version": "2.0",
"meta": {
"generated_at": "2026-01-16T14:15:17.461Z",
"slug": "0xdarkmatter-python-typing-patterns",
"source_url": "https://github.com/0xDarkMatter/claude-mods/tree/main/skills/python-typing-patterns",
"source_ref": "main",
"model": "claude",
"analysis_version": "3.0.0",
"source_type": "community",
"content_hash": "7365b3b15c2018d2a44fe44806bbe7bbba7cdbeb375978698c6c92ae6cae0942",
"tree_hash": "a222249d22762172fed9ecc881a478c761a081ade8c989f9188a38d517da3f9f"
},
"skill": {
"name": "python-typing-patterns",
"description": "Python type hints and type safety patterns. Triggers on: type hints, typing, TypeVar, Generic, Protocol, mypy, pyright, type annotation, overload, TypedDict.",
"summary": "Python type hints and type safety patterns. Triggers on: type hints, typing, TypeVar, Generic, Proto...",
"icon": "🐍",
"version": "1.0.0",
"author": "0xDarkMatter",
"license": "MIT",
"category": "documentation",
"tags": [
"python",
"typing",
"type-safety",
"mypy",
"pyright"
],
"supported_tools": [
"claude",
"codex",
"claude-code"
],
"risk_factors": [
"network",
"external_commands",
"scripts",
"filesystem"
]
},
"security_audit": {
"risk_level": "safe",
"is_blocked": false,
"safe_to_publish": true,
"summary": "This is a documentation/reference skill containing only Python typing patterns. All 293 static findings are false positives. The scanner misidentified markdown code formatting (backticks) as shell execution, TOML config keys as cryptographic algorithms, and Python typing library names as sensitive keywords. No actual network calls, credential handling, or command execution exists.",
"risk_factor_evidence": [
{
"factor": "network",
"evidence": [
{
"file": "assets/pyproject-typing.toml",
"line_start": 56,
"line_end": 56
},
{
"file": "references/mypy-config.md",
"line_start": 33,
"line_end": 33
},
{
"file": "references/mypy-config.md",
"line_start": 53,
"line_end": 53
},
{
"file": "references/mypy-config.md",
"line_start": 246,
"line_end": 246
},
{
"file": "references/overloads.md",
"line_start": 42,
"line_end": 42
},
{
"file": "references/overloads.md",
"line_start": 45,
"line_end": 45
},
{
"file": "references/overloads.md",
"line_start": 48,
"line_end": 48
},
{
"file": "references/overloads.md",
"line_start": 50,
"line_end": 50
},
{
"file": "references/overloads.md",
"line_start": 61,
"line_end": 61
},
{
"file": "references/overloads.md",
"line_start": 62,
"line_end": 62
},
{
"file": "references/overloads.md",
"line_start": 51,
"line_end": 51
},
{
"file": "references/overloads.md",
"line_start": 61,
"line_end": 61
},
{
"file": "references/overloads.md",
"line_start": 62,
"line_end": 62
},
{
"file": "skill-report.json",
"line_start": 6,
"line_end": 6
},
{
"file": "SKILL.md",
"line_start": 62,
"line_end": 62
}
]
},
{
"factor": "external_commands",
"evidence": [
{
"file": "references/generics-advanced.md",
"line_start": 7,
"line_end": 19
},
{
"file": "references/generics-advanced.md",
"line_start": 19,
"line_end": 23
},
{
"file": "references/generics-advanced.md",
"line_start": 23,
"line_end": 43
},
{
"file": "references/generics-advanced.md",
"line_start": 43,
"line_end": 47
},
{
"file": "references/generics-advanced.md",
"line_start": 47,
"line_end": 60
},
{
"file": "references/generics-advanced.md",
"line_start": 60,
"line_end": 64
},
{
"file": "references/generics-advanced.md",
"line_start": 64,
"line_end": 90
},
{
"file": "references/generics-advanced.md",
"line_start": 90,
"line_end": 94
},
{
"file": "references/generics-advanced.md",
"line_start": 94,
"line_end": 110
},
{
"file": "references/generics-advanced.md",
"line_start": 110,
"line_end": 114
},
{
"file": "references/generics-advanced.md",
"line_start": 114,
"line_end": 135
},
{
"file": "references/generics-advanced.md",
"line_start": 135,
"line_end": 139
},
{
"file": "references/generics-advanced.md",
"line_start": 139,
"line_end": 159
},
{
"file": "references/generics-advanced.md",
"line_start": 159,
"line_end": 163
},
{
"file": "references/generics-advanced.md",
"line_start": 163,
"line_end": 177
},
{
"file": "references/generics-advanced.md",
"line_start": 177,
"line_end": 181
},
{
"file": "references/generics-advanced.md",
"line_start": 181,
"line_end": 206
},
{
"file": "references/generics-advanced.md",
"line_start": 206,
"line_end": 210
},
{
"file": "references/generics-advanced.md",
"line_start": 210,
"line_end": 227
},
{
"file": "references/generics-advanced.md",
"line_start": 227,
"line_end": 231
},
{
"file": "references/generics-advanced.md",
"line_start": 231,
"line_end": 249
},
{
"file": "references/generics-advanced.md",
"line_start": 249,
"line_end": 253
},
{
"file": "references/generics-advanced.md",
"line_start": 253,
"line_end": 272
},
{
"file": "references/generics-advanced.md",
"line_start": 272,
"line_end": 276
},
{
"file": "references/generics-advanced.md",
"line_start": 276,
"line_end": 276
},
{
"file": "references/generics-advanced.md",
"line_start": 276,
"line_end": 276
},
{
"file": "references/generics-advanced.md",
"line_start": 276,
"line_end": 276
},
{
"file": "references/generics-advanced.md",
"line_start": 276,
"line_end": 276
},
{
"file": "references/mypy-config.md",
"line_start": 9,
"line_end": 38
},
{
"file": "references/mypy-config.md",
"line_start": 38,
"line_end": 42
},
{
"file": "references/mypy-config.md",
"line_start": 42,
"line_end": 55
},
{
"file": "references/mypy-config.md",
"line_start": 55,
"line_end": 61
},
{
"file": "references/mypy-config.md",
"line_start": 61,
"line_end": 78
},
{
"file": "references/mypy-config.md",
"line_start": 78,
"line_end": 82
},
{
"file": "references/mypy-config.md",
"line_start": 82,
"line_end": 102
},
{
"file": "references/mypy-config.md",
"line_start": 102,
"line_end": 108
},
{
"file": "references/mypy-config.md",
"line_start": 108,
"line_end": 130
},
{
"file": "references/mypy-config.md",
"line_start": 130,
"line_end": 134
},
{
"file": "references/mypy-config.md",
"line_start": 134,
"line_end": 141
},
{
"file": "references/mypy-config.md",
"line_start": 141,
"line_end": 147
},
{
"file": "references/mypy-config.md",
"line_start": 147,
"line_end": 154
},
{
"file": "references/mypy-config.md",
"line_start": 154,
"line_end": 158
},
{
"file": "references/mypy-config.md",
"line_start": 158,
"line_end": 170
},
{
"file": "references/mypy-config.md",
"line_start": 170,
"line_end": 174
},
{
"file": "references/mypy-config.md",
"line_start": 174,
"line_end": 182
},
{
"file": "references/mypy-config.md",
"line_start": 182,
"line_end": 186
},
{
"file": "references/mypy-config.md",
"line_start": 186,
"line_end": 193
},
{
"file": "references/mypy-config.md",
"line_start": 193,
"line_end": 195
},
{
"file": "references/mypy-config.md",
"line_start": 195,
"line_end": 198
},
{
"file": "references/mypy-config.md",
"line_start": 198,
"line_end": 204
},
{
"file": "references/mypy-config.md",
"line_start": 204,
"line_end": 239
},
{
"file": "references/mypy-config.md",
"line_start": 239,
"line_end": 243
},
{
"file": "references/mypy-config.md",
"line_start": 243,
"line_end": 252
},
{
"file": "references/mypy-config.md",
"line_start": 252,
"line_end": 256
},
{
"file": "references/mypy-config.md",
"line_start": 256,
"line_end": 265
},
{
"file": "references/mypy-config.md",
"line_start": 265,
"line_end": 271
},
{
"file": "references/mypy-config.md",
"line_start": 271,
"line_end": 276
},
{
"file": "references/mypy-config.md",
"line_start": 276,
"line_end": 280
},
{
"file": "references/mypy-config.md",
"line_start": 280,
"line_end": 286
},
{
"file": "references/mypy-config.md",
"line_start": 286,
"line_end": 290
},
{
"file": "references/mypy-config.md",
"line_start": 290,
"line_end": 299
},
{
"file": "references/mypy-config.md",
"line_start": 299,
"line_end": 305
},
{
"file": "references/mypy-config.md",
"line_start": 305,
"line_end": 306
},
{
"file": "references/mypy-config.md",
"line_start": 306,
"line_end": 307
},
{
"file": "references/mypy-config.md",
"line_start": 307,
"line_end": 308
},
{
"file": "references/mypy-config.md",
"line_start": 308,
"line_end": 309
},
{
"file": "references/mypy-config.md",
"line_start": 309,
"line_end": 310
},
{
"file": "references/mypy-config.md",
"line_start": 310,
"line_end": 314
},
{
"file": "references/mypy-config.md",
"line_start": 314,
"line_end": 315
},
{
"file": "references/mypy-config.md",
"line_start": 315,
"line_end": 316
},
{
"file": "references/mypy-config.md",
"line_start": 316,
"line_end": 317
},
{
"file": "references/overloads.md",
"line_start": 7,
"line_end": 34
},
{
"file": "references/overloads.md",
"line_start": 34,
"line_end": 38
},
{
"file": "references/overloads.md",
"line_start": 38,
"line_end": 63
},
{
"file": "references/overloads.md",
"line_start": 63,
"line_end": 67
},
{
"file": "references/overloads.md",
"line_start": 67,
"line_end": 90
},
{
"file": "references/overloads.md",
"line_start": 90,
"line_end": 94
},
{
"file": "references/overloads.md",
"line_start": 94,
"line_end": 119
},
{
"file": "references/overloads.md",
"line_start": 119,
"line_end": 123
},
{
"file": "references/overloads.md",
"line_start": 123,
"line_end": 147
},
{
"file": "references/overloads.md",
"line_start": 147,
"line_end": 151
},
{
"file": "references/overloads.md",
"line_start": 151,
"line_end": 176
},
{
"file": "references/overloads.md",
"line_start": 176,
"line_end": 180
},
{
"file": "references/overloads.md",
"line_start": 180,
"line_end": 195
},
{
"file": "references/overloads.md",
"line_start": 195,
"line_end": 199
},
{
"file": "references/overloads.md",
"line_start": 199,
"line_end": 227
},
{
"file": "references/overloads.md",
"line_start": 227,
"line_end": 231
},
{
"file": "references/overloads.md",
"line_start": 231,
"line_end": 255
},
{
"file": "references/overloads.md",
"line_start": 255,
"line_end": 261
},
{
"file": "references/overloads.md",
"line_start": 261,
"line_end": 262
},
{
"file": "references/overloads.md",
"line_start": 262,
"line_end": 263
},
{
"file": "references/overloads.md",
"line_start": 263,
"line_end": 263
},
{
"file": "references/overloads.md",
"line_start": 263,
"line_end": 268
},
{
"file": "references/protocols-patterns.md",
"line_start": 7,
"line_end": 28
},
{
"file": "references/protocols-patterns.md",
"line_start": 28,
"line_end": 32
},
{
"file": "references/protocols-patterns.md",
"line_start": 32,
"line_end": 52
},
{
"file": "references/protocols-patterns.md",
"line_start": 52,
"line_end": 56
},
{
"file": "references/protocols-patterns.md",
"line_start": 56,
"line_end": 74
},
{
"file": "references/protocols-patterns.md",
"line_start": 74,
"line_end": 78
},
{
"file": "references/protocols-patterns.md",
"line_start": 78,
"line_end": 97
},
{
"file": "references/protocols-patterns.md",
"line_start": 97,
"line_end": 101
},
{
"file": "references/protocols-patterns.md",
"line_start": 101,
"line_end": 129
},
{
"file": "references/protocols-patterns.md",
"line_start": 129,
"line_end": 133
},
{
"file": "references/protocols-patterns.md",
"line_start": 133,
"line_end": 150
},
{
"file": "references/protocols-patterns.md",
"line_start": 150,
"line_end": 154
},
{
"file": "references/protocols-patterns.md",
"line_start": 154,
"line_end": 182
},
{
"file": "references/protocols-patterns.md",
"line_start": 182,
"line_end": 188
},
{
"file": "references/protocols-patterns.md",
"line_start": 188,
"line_end": 196
},
{
"file": "references/protocols-patterns.md",
"line_start": 196,
"line_end": 200
},
{
"file": "references/protocols-patterns.md",
"line_start": 200,
"line_end": 212
},
{
"file": "references/protocols-patterns.md",
"line_start": 212,
"line_end": 216
},
{
"file": "references/protocols-patterns.md",
"line_start": 216,
"line_end": 232
},
{
"file": "references/protocols-patterns.md",
"line_start": 232,
"line_end": 238
},
{
"file": "references/protocols-patterns.md",
"line_start": 238,
"line_end": 274
},
{
"file": "references/protocols-patterns.md",
"line_start": 274,
"line_end": 278
},
{
"file": "references/protocols-patterns.md",
"line_start": 278,
"line_end": 307
},
{
"file": "references/runtime-validation.md",
"line_start": 7,
"line_end": 44
},
{
"file": "references/runtime-validation.md",
"line_start": 44,
"line_end": 48
},
{
"file": "references/runtime-validation.md",
"line_start": 48,
"line_end": 67
},
{
"file": "references/runtime-validation.md",
"line_start": 67,
"line_end": 71
},
{
"file": "references/runtime-validation.md",
"line_start": 71,
"line_end": 105
},
{
"file": "references/runtime-validation.md",
"line_start": 105,
"line_end": 109
},
{
"file": "references/runtime-validation.md",
"line_start": 109,
"line_end": 133
},
{
"file": "references/runtime-validation.md",
"line_start": 133,
"line_end": 137
},
{
"file": "references/runtime-validation.md",
"line_start": 137,
"line_end": 159
},
{
"file": "references/runtime-validation.md",
"line_start": 159,
"line_end": 163
},
{
"file": "references/runtime-validation.md",
"line_start": 163,
"line_end": 183
},
{
"file": "references/runtime-validation.md",
"line_start": 183,
"line_end": 187
},
{
"file": "references/runtime-validation.md",
"line_start": 187,
"line_end": 214
},
{
"file": "references/runtime-validation.md",
"line_start": 214,
"line_end": 218
},
{
"file": "references/runtime-validation.md",
"line_start": 218,
"line_end": 245
},
{
"file": "references/runtime-validation.md",
"line_start": 245,
"line_end": 255
},
{
"file": "references/runtime-validation.md",
"line_start": 255,
"line_end": 280
},
{
"file": "references/runtime-validation.md",
"line_start": 280,
"line_end": 286
},
{
"file": "references/runtime-validation.md",
"line_start": 286,
"line_end": 286
},
{
"file": "references/runtime-validation.md",
"line_start": 286,
"line_end": 287
},
{
"file": "references/runtime-validation.md",
"line_start": 287,
"line_end": 287
},
{
"file": "references/runtime-validation.md",
"line_start": 287,
"line_end": 288
},
{
"file": "references/runtime-validation.md",
"line_start": 288,
"line_end": 292
},
{
"file": "references/runtime-validation.md",
"line_start": 292,
"line_end": 292
},
{
"file": "references/runtime-validation.md",
"line_start": 292,
"line_end": 293
},
{
"file": "references/runtime-validation.md",
"line_start": 293,
"line_end": 293
},
{
"file": "references/runtime-validation.md",
"line_start": 293,
"line_end": 294
},
{
"file": "references/runtime-validation.md",
"line_start": 294,
"line_end": 294
},
{
"file": "references/runtime-validation.md",
"line_start": 294,
"line_end": 295
},
{
"file": "references/runtime-validation.md",
"line_start": 295,
"line_end": 296
},
{
"file": "references/runtime-validation.md",
"line_start": 296,
"line_end": 297
},
{
"file": "references/type-narrowing.md",
"line_start": 7,
"line_end": 18
},
{
"file": "references/type-narrowing.md",
"line_start": 18,
"line_end": 22
},
{
"file": "references/type-narrowing.md",
"line_start": 22,
"line_end": 35
},
{
"file": "references/type-narrowing.md",
"line_start": 35,
"line_end": 39
},
{
"file": "references/type-narrowing.md",
"line_start": 39,
"line_end": 49
},
{
"file": "references/type-narrowing.md",
"line_start": 49,
"line_end": 53
},
{
"file": "references/type-narrowing.md",
"line_start": 53,
"line_end": 79
},
{
"file": "references/type-narrowing.md",
"line_start": 79,
"line_end": 83
},
{
"file": "references/type-narrowing.md",
"line_start": 83,
"line_end": 95
},
{
"file": "references/type-narrowing.md",
"line_start": 95,
"line_end": 99
},
{
"file": "references/type-narrowing.md",
"line_start": 99,
"line_end": 119
},
{
"file": "references/type-narrowing.md",
"line_start": 119,
"line_end": 123
},
{
"file": "references/type-narrowing.md",
"line_start": 123,
"line_end": 134
},
{
"file": "references/type-narrowing.md",
"line_start": 134,
"line_end": 138
},
{
"file": "references/type-narrowing.md",
"line_start": 138,
"line_end": 150
},
{
"file": "references/type-narrowing.md",
"line_start": 150,
"line_end": 154
},
{
"file": "references/type-narrowing.md",
"line_start": 154,
"line_end": 164
},
{
"file": "references/type-narrowing.md",
"line_start": 164,
"line_end": 168
},
{
"file": "references/type-narrowing.md",
"line_start": 168,
"line_end": 186
},
{
"file": "references/type-narrowing.md",
"line_start": 186,
"line_end": 190
},
{
"file": "references/type-narrowing.md",
"line_start": 190,
"line_end": 208
},
{
"file": "references/type-narrowing.md",
"line_start": 208,
"line_end": 212
},
{
"file": "references/type-narrowing.md",
"line_start": 212,
"line_end": 230
},
{
"file": "references/type-narrowing.md",
"line_start": 230,
"line_end": 236
},
{
"file": "references/type-narrowing.md",
"line_start": 236,
"line_end": 248
},
{
"file": "references/type-narrowing.md",
"line_start": 248,
"line_end": 252
},
{
"file": "references/type-narrowing.md",
"line_start": 252,
"line_end": 262
},
{
"file": "references/type-narrowing.md",
"line_start": 262,
"line_end": 268
},
{
"file": "references/type-narrowing.md",
"line_start": 268,
"line_end": 268
},
{
"file": "scripts/check-types.sh",
"line_start": 87,
"line_end": 87
},
{
"file": "scripts/check-types.sh",
"line_start": 1,
"line_end": 1
},
{
"file": "SKILL.md",
"line_start": 16,
"line_end": 30
},
{
"file": "SKILL.md",
"line_start": 30,
"line_end": 34
},
{
"file": "SKILL.md",
"line_start": 34,
"line_end": 49
},
{
"file": "SKILL.md",
"line_start": 49,
"line_end": 53
},
{
"file": "SKILL.md",
"line_start": 53,
"line_end": 64
},
{
"file": "SKILL.md",
"line_start": 64,
"line_end": 68
},
{
"file": "SKILL.md",
"line_start": 68,
"line_end": 86
},
{
"file": "SKILL.md",
"line_start": 86,
"line_end": 90
},
{
"file": "SKILL.md",
"line_start": 90,
"line_end": 105
},
{
"file": "SKILL.md",
"line_start": 105,
"line_end": 109
},
{
"file": "SKILL.md",
"line_start": 109,
"line_end": 124
},
{
"file": "SKILL.md",
"line_start": 124,
"line_end": 128
},
{
"file": "SKILL.md",
"line_start": 128,
"line_end": 143
},
{
"file": "SKILL.md",
"line_start": 143,
"line_end": 147
},
{
"file": "SKILL.md",
"line_start": 147,
"line_end": 157
},
{
"file": "SKILL.md",
"line_start": 157,
"line_end": 161
},
{
"file": "SKILL.md",
"line_start": 161,
"line_end": 172
},
{
"file": "SKILL.md",
"line_start": 172,
"line_end": 178
},
{
"file": "SKILL.md",
"line_start": 178,
"line_end": 179
},
{
"file": "SKILL.md",
"line_start": 179,
"line_end": 180
},
{
"file": "SKILL.md",
"line_start": 180,
"line_end": 181
},
{
"file": "SKILL.md",
"line_start": 181,
"line_end": 182
},
{
"file": "SKILL.md",
"line_start": 182,
"line_end": 183
},
{
"file": "SKILL.md",
"line_start": 183,
"line_end": 184
},
{
"file": "SKILL.md",
"line_start": 184,
"line_end": 185
},
{
"file": "SKILL.md",
"line_start": 185,
"line_end": 186
},
{
"file": "SKILL.md",
"line_start": 186,
"line_end": 190
},
{
"file": "SKILL.md",
"line_start": 190,
"line_end": 201
},
{
"file": "SKILL.md",
"line_start": 201,
"line_end": 205
},
{
"file": "SKILL.md",
"line_start": 205,
"line_end": 206
},
{
"file": "SKILL.md",
"line_start": 206,
"line_end": 207
},
{
"file": "SKILL.md",
"line_start": 207,
"line_end": 208
},
{
"file": "SKILL.md",
"line_start": 208,
"line_end": 209
},
{
"file": "SKILL.md",
"line_start": 209,
"line_end": 210
},
{
"file": "SKILL.md",
"line_start": 210,
"line_end": 214
},
{
"file": "SKILL.md",
"line_start": 214,
"line_end": 218
},
{
"file": "SKILL.md",
"line_start": 218,
"line_end": 227
},
{
"file": "SKILL.md",
"line_start": 227,
"line_end": 230
},
{
"file": "SKILL.md",
"line_start": 230,
"line_end": 231
},
{
"file": "SKILL.md",
"line_start": 231,
"line_end": 232
}
]
},
{
"factor": "scripts",
"evidence": [
{
"file": "references/runtime-validation.md",
"line_start": 219,
"line_end": 226
}
]
},
{
"factor": "filesystem",
"evidence": [
{
"file": "scripts/check-types.sh",
"line_start": 54,
"line_end": 54
},
{
"file": "scripts/check-types.sh",
"line_start": 79,
"line_end": 79
},
{
"file": "scripts/check-types.sh",
"line_start": 87,
"line_end": 87
}
]
}
],
"critical_findings": [],
"high_findings": [],
"medium_findings": [],
"low_findings": [],
"dangerous_patterns": [],
"files_scanned": 10,
"total_lines": 2545,
"audit_model": "claude",
"audited_at": "2026-01-16T14:15:17.461Z"
},
"content": {
"user_title": "Apply Python Type Hints Effectively",
"value_statement": "Python type hints improve code quality and catch bugs early. This skill provides patterns for TypeVar, Generic, Protocol, TypedDict, and runtime validation with Pydantic and typeguard.",
"seo_keywords": [
"python typing patterns",
"python type hints",
"TypeVar Generic",
"Protocol Python",
"mypy configuration",
"pyright",
"TypedDict patterns",
"runtime type validation",
"Claude Code",
"Python Codex"
],
"actual_capabilities": [
"Shows Python type annotation syntax for variables, functions, and classes",
"Explains TypeVar, Generic, Protocol, and ParamSpec for flexible types",
"Demonstrates TypedDict for type-safe dictionary access",
"Covers mypy and pyright configuration for strict type checking",
"Provides runtime validation patterns using Pydantic, typeguard, and beartype"
],
"limitations": [
"Does not include type checker installation or execution",
"Does not modify user source code automatically",
"Does not integrate with IDEs or build systems directly",
"Requires Python 3.10+ for modern union syntax"
],
"use_cases": [
{
"target_user": "Python developers",
"title": "Add type hints to code",
"description": "Learn correct syntax for type annotations in Python 3.10+ including generics and protocols."
},
{
"target_user": "Library maintainers",
"title": "Design type-safe APIs",
"description": "Create flexible APIs using TypeVar and Protocol for structural typing without inheritance."
},
{
"target_user": "Backend developers",
"title": "Validate API data",
"description": "Use Pydantic and typeguard to enforce types at runtime for API requests and config."
}
],
"prompt_templates": [
{
"title": "Basic typing syntax",
"scenario": "Learning type annotations",
"prompt": "Show me how to write type hints for a function that takes a list of strings and returns an integer."
},
{
"title": "Generic functions",
"scenario": "Creating reusable functions",
"prompt": "How do I use TypeVar to create a function that works with any type but preserves the type information?"
},
{
"title": "Protocol pattern",
"scenario": "Structural typing",
"prompt": "Explain how Protocol works in Python and show an example where an object without inheritance satisfies an interface."
},
{
"title": "Runtime validation",
"scenario": "API data validation",
"prompt": "How do I validate function arguments at runtime using Pydantic v2 with field constraints?"
}
],
"output_examples": [
{
"input": "How do I type a function that returns the same type as its input?",
"output": [
"Use TypeVar to preserve the input type:",
"```python",
"from typing import TypeVar",
"T = TypeVar('T')",
"def identity(x: T) -> T:",
" return x",
"```",
"Result: identity(42) returns int, identity('hello') returns str"
]
},
{
"input": "How do I validate that a user email is valid at runtime?",
"output": [
"Use Pydantic with EmailStr:",
"```python",
"from pydantic import BaseModel, EmailStr",
"class User(BaseModel):",
" email: EmailStr",
"```",
"This automatically validates and normalizes email addresses when creating User instances."
]
}
],
"best_practices": [
"Use Python 3.10+ union syntax (X | Y) instead of Optional[X] for clarity",
"Prefer Protocol over ABC for structural typing to allow duck typing",
"Enable strict mode in mypy or pyright to catch issues early"
],
"anti_patterns": [
"Using Any extensively defeats the purpose of type checking",
"Avoid quotes around forward references (use from __future__ import annotations)",
"Do not mix Optional[X] with X | None inconsistently in the same codebase"
],
"faq": [
{
"question": "What Python version do I need?",
"answer": "Python 3.10+ for union syntax. Python 3.11+ for Self and TypeVarTuple."
},
{
"question": "mypy vs pyright?",
"answer": "mypy is slower but mature. pyright is faster with better IDE integration."
},
{
"question": "Type hints slow?",
"answer": "No, type hints are stripped at runtime. Runtime validation adds overhead."
},
{
"question": "TypedDict vs dict?",
"answer": "TypedDict validates keys exist. Regular dict allows any keys with any types."
},
{
"question": "Protocol inheritance?",
"answer": "Protocol uses structural typing. No explicit inheritance needed."
},
{
"question": "Required vs NotRequired?",
"answer": "Required keys must exist. NotRequired keys are optional in TypedDict."
}
]
},
"file_structure": [
{
"name": "assets",
"type": "dir",
"path": "assets",
"children": [
{
"name": "pyproject-typing.toml",
"type": "file",
"path": "assets/pyproject-typing.toml",
"lines": 118
}
]
},
{
"name": "references",
"type": "dir",
"path": "references",
"children": [
{
"name": "generics-advanced.md",
"type": "file",
"path": "references/generics-advanced.md",
"lines": 283
},
{
"name": "mypy-config.md",
"type": "file",
"path": "references/mypy-config.md",
"lines": 318
},
{
"name": "overloads.md",
"type": "file",
"path": "references/overloads.md",
"lines": 272
},
{
"name": "protocols-patterns.md",
"type": "file",
"path": "references/protocols-patterns.md",
"lines": 317
},
{
"name": "runtime-validation.md",
"type": "file",
"path": "references/runtime-validation.md",
"lines": 298
},
{
"name": "type-narrowing.md",
"type": "file",
"path": "references/type-narrowing.md",
"lines": 272
}
]
},
{
"name": "scripts",
"type": "dir",
"path": "scripts",
"children": [
{
"name": "check-types.sh",
"type": "file",
"path": "scripts/check-types.sh",
"lines": 152
}
]
},
{
"name": "SKILL.md",
"type": "file",
"path": "SKILL.md",
"lines": 233
}
]
}
Related skills
FAQ
Which Python version does this target?
Python 3.10+ using the X | Y union syntax, with some patterns needing 3.11+.
Does it cover runtime validation?
Its references mention Pydantic v2, typeguard, and beartype for runtime validation.