.agents/skills/dignified-python/versions/python-3.10.md
This document captures type annotation guidance for Python 3.10. This is the baseline for modern Python type syntax.
Python 3.10 introduced major improvements to type annotation syntax through PEP 604 (union types via
|) and PEP 585 (generic types in standard collections). These features eliminated the need for
most typing module imports and made type annotations more concise and readable.
What's new in 3.10:
| operator (PEP 604)list[T], dict[K, V], etc. (PEP 585)List, Dict, Union, Optional from typingWhat you need from typing module:
TypeVar for generic functions/classesProtocol for structural typing (rare - prefer ABC)TYPE_CHECKING for conditional importsAny (use sparingly)✅ PREFERRED - Use built-in generic types:
names: list[str] = []
mapping: dict[str, int] = {}
unique_ids: set[str] = set()
coordinates: tuple[int, int] = (0, 0)
❌ WRONG - Don't use typing module equivalents:
from typing import List, Dict, Set, Tuple # Don't do this
names: List[str] = []
mapping: Dict[str, int] = {}
Why: Built-in types are more concise, don't require imports, and are the modern Python standard.
✅ PREFERRED - Use | operator:
def process(value: str | int) -> str:
return str(value)
def find_config(name: str) -> dict[str, str] | dict[str, int]:
...
# Multiple unions
def parse(input: str | int | float) -> str:
return str(input)
❌ WRONG - Don't use typing.Union:
from typing import Union
def process(value: Union[str, int]) -> str: # Don't do this
...
✅ PREFERRED - Use X | None:
def find_user(id: str) -> User | None:
"""Returns user or None if not found."""
if id in users:
return users[id]
return None
def get_config(key: str) -> str | None:
return config.get(key)
❌ WRONG - Don't use typing.Optional:
from typing import Optional
def find_user(id: str) -> Optional[User]: # Don't do this
...
✅ PREFERRED - Use TypeVar for generic functions:
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
"""Return first item or None if empty."""
if not items:
return None
return items[0]
def identity(value: T) -> T:
"""Return the value unchanged."""
return value
Note: This is the standard way in Python 3.10. Python 3.12 introduces better syntax (PEP 695).
✅ PREFERRED - Use Generic with TypeVar:
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
"""A generic stack data structure."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T | None:
if not self._items:
return None
return self._items.pop()
# Usage
int_stack = Stack[int]()
int_stack.push(42)
Note: Python 3.12 introduces cleaner syntax for this pattern.
✅ Use TypeVar constraints when needed:
from typing import TypeVar
# Constrained to specific types
Numeric = TypeVar("Numeric", int, float)
def add(a: Numeric, b: Numeric) -> Numeric:
return a + b
# Bounded to base class
T = TypeVar("T", bound=BaseClass)
def process(obj: T) -> T:
return obj
✅ PREFERRED - Use collections.abc.Callable:
from collections.abc import Callable
# Function that takes int, returns str
processor: Callable[[int], str] = str
# Function with no args, returns None
callback: Callable[[], None] = lambda: None
# Function with multiple args
validator: Callable[[str, int], bool] = lambda s, i: len(s) > i
✅ Use simple assignment for type aliases:
# Simple alias
UserId = str
Config = dict[str, str | int | bool]
# Complex nested type
JsonValue = dict[str, "JsonValue"] | list["JsonValue"] | str | int | float | bool | None
def load_config() -> Config:
return {"host": "localhost", "port": 8080}
Note: Python 3.12 introduces type statement for better alias support.
Use from __future__ import annotations when you encounter:
Forward references (class referencing itself):
from __future__ import annotations
class Node:
def __init__(self, value: int, parent: Node | None = None):
self.value = value
self.parent = parent
Circular type imports:
# a.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import B
class A:
def method(self) -> B:
...
Complex recursive types:
from __future__ import annotations
JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
✅ PREFERRED - Use ABC for interfaces:
from abc import ABC, abstractmethod
class Repository(ABC):
@abstractmethod
def get(self, id: str) -> User | None:
"""Get user by ID."""
@abstractmethod
def save(self, user: User) -> None:
"""Save user."""
🟡 VALID - Use Protocol only for structural typing:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
# Any object with draw() method matches
def render(obj: Drawable) -> None:
obj.draw()
Dignified Python prefers ABC because it makes inheritance and intent explicit.
from abc import ABC, abstractmethod
class Repository(ABC):
"""Abstract base class for data repositories."""
@abstractmethod
def get(self, id: str) -> dict[str, str] | None:
"""Get entity by ID."""
@abstractmethod
def save(self, entity: dict[str, str]) -> None:
"""Save entity."""
@abstractmethod
def delete(self, id: str) -> bool:
"""Delete entity, return success."""
class UserRepository(Repository):
def __init__(self) -> None:
self._users: dict[str, dict[str, str]] = {}
def get(self, id: str) -> dict[str, str] | None:
return self._users.get(id)
def save(self, entity: dict[str, str]) -> None:
if "id" not in entity:
raise ValueError("Entity must have id")
self._users[entity["id"]] = entity
def delete(self, id: str) -> bool:
if id in self._users:
del self._users[id]
return True
return False
from typing import Generic, TypeVar
T = TypeVar("T")
class Node(Generic[T]):
"""A node in a tree structure."""
def __init__(self, value: T, children: list[Node[T]] | None = None) -> None:
self.value = value
self.children = children or []
def add_child(self, child: Node[T]) -> None:
self.children.append(child)
def find(self, predicate: Callable[[T], bool]) -> Node[T] | None:
"""Find first node matching predicate."""
if predicate(self.value):
return self
for child in self.children:
result = child.find(predicate)
if result:
return result
return None
# Usage
from collections.abc import Callable
root = Node[int](1)
root.add_child(Node[int](2))
root.add_child(Node[int](3))
from dataclasses import dataclass
@dataclass(frozen=True)
class DatabaseConfig:
host: str
port: int
username: str
password: str | None = None
ssl_enabled: bool = False
@dataclass(frozen=True)
class AppConfig:
app_name: str
debug_mode: bool
database: DatabaseConfig
feature_flags: dict[str, bool]
def load_config(path: str) -> AppConfig:
"""Load application configuration from file."""
import json
from pathlib import Path
config_path = Path(path)
if not config_path.exists():
raise FileNotFoundError(f"Config not found: {path}")
data: dict[str, str | int | bool | dict[str, str | int | bool]] = json.loads(
config_path.read_text(encoding="utf-8")
)
# Parse and validate...
return AppConfig(...)
from collections.abc import Callable
from typing import TypeVar
T = TypeVar("T")
class ApiResponse(Generic[T]):
"""Container for API response with data or error."""
def __init__(self, data: T | None = None, error: str | None = None) -> None:
self.data = data
self.error = error
def is_success(self) -> bool:
return self.error is None
def map(self, func: Callable[[T], U]) -> ApiResponse[U]:
"""Transform successful response data."""
if self.is_success() and self.data is not None:
return ApiResponse(data=func(self.data))
return ApiResponse(error=self.error)
U = TypeVar("U")
def fetch_user(id: str) -> ApiResponse[dict[str, str]]:
"""Fetch user from API."""
# Implementation...
return ApiResponse(data={"id": id, "name": "Alice"})
✅ MUST type:
self, cls)🟡 SHOULD type:
🟢 MAY skip:
count = 0)uv run ty check
All code should pass type checking without errors.
Configure ty in pyproject.toml:
[tool.ty.environment]
python-version = "3.10"
✅ CORRECT - Check before use:
def process_user(user: User | None) -> str:
if user is None:
return "No user"
return user.name
✅ CORRECT - Handle None case:
def get_port(config: dict[str, int]) -> int:
port = config.get("port")
if port is None:
return 8080
return port
✅ CORRECT - Check before accessing:
def first_or_default(items: list[str], default: str) -> str:
if not items:
return default
return items[0]
If upgrading from Python 3.9, apply these changes:
Replace typing module types:
List[X] → list[X]Dict[K, V] → dict[K, V]Set[X] → set[X]Tuple[X, Y] → tuple[X, Y]Union[X, Y] → X | YOptional[X] → X | NoneAdd future annotations if needed:
from __future__ import annotations for forward referencesTYPE_CHECKINGRemove unnecessary imports:
from typing import List, Dict, Optional, UnionTypeVar, Generic, Protocol, TYPE_CHECKING, Any