.agents/skills/dignified-python/versions/python-3.12.md
This document captures type annotation guidance for Python 3.12.
Python 3.12 introduces PEP 695, a major syntactic improvement for generic types. The new type parameter syntax makes generic functions and classes significantly more readable. All syntax from 3.10 and 3.11 continues to work.
What's new in 3.12:
def func[T](x: T) -> Ttype statement for better type aliasesAvailable from 3.11:
Self type for self-returning methodsAvailable from 3.10:
list[T], dict[K, V], etc.| operatorX | NoneWhat you need from typing module:
Self for self-returning methodsTypeVar only for constrained/bounded genericsProtocol 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] = []
✅ 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
❌ WRONG - Don't use typing.Optional:
from typing import Optional
def find_user(id: str) -> Optional[User]: # Don't do this
...
✅ PREFERRED - Use Self for methods that return the instance:
from typing import Self
class Builder:
def set_name(self, name: str) -> Self:
self.name = name
return self
def set_value(self, value: int) -> Self:
self.value = value
return self
✅ PREFERRED - Use PEP 695 type parameter syntax:
def first[T](items: list[T]) -> T | None:
"""Return first item or None if empty."""
if not items:
return None
return items[0]
def identity[T](value: T) -> T:
"""Return value unchanged."""
return value
# Multiple type parameters
def zip_dicts[K, V](keys: list[K], values: list[V]) -> dict[K, V]:
"""Create dict from separate key and value lists."""
return dict(zip(keys, values))
🟡 VALID - TypeVar still works:
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
if not items:
return None
return items[0]
Note: Prefer PEP 695 syntax for simple generics. TypeVar is still needed for constraints/bounds.
✅ PREFERRED - Use PEP 695 class syntax:
class Stack[T]:
"""A generic stack data structure."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> Self:
self._items.append(item)
return self
def pop(self) -> T | None:
if not self._items:
return None
return self._items.pop()
# Usage
int_stack = Stack[int]()
int_stack.push(42).push(43)
🟡 VALID - Generic with TypeVar still works:
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
# ... rest of implementation
Note: PEP 695 is cleaner - no imports needed, type parameter scope is local to class.
✅ Use bounds with PEP 695:
class Comparable:
def compare(self, other: object) -> int:
...
def max_value[T: Comparable](items: list[T]) -> T:
"""Get maximum value from comparable items."""
return max(items, key=lambda x: x)
✅ Use TypeVar for specific type constraints:
from typing import TypeVar
# Constrained to specific types - must use TypeVar
Numeric = TypeVar("Numeric", int, float)
def add(a: Numeric, b: Numeric) -> Numeric:
return a + b
❌ WRONG - PEP 695 doesn't support constraints:
# This doesn't constrain to int|float
def add[Numeric](a: Numeric, b: Numeric) -> Numeric:
return a + b
✅ PREFERRED - Use type statement:
# Simple alias
type UserId = str
type Config = dict[str, str | int | bool]
# Generic type alias
type Result[T] = tuple[T, str | None]
def process(value: str) -> Result[int]:
try:
return (int(value), None)
except ValueError as e:
return (0, str(e))
🟡 VALID - Simple assignment still works:
UserId = str # Still valid
Config = dict[str, str | int | bool] # Still valid
Note: type statement is more explicit and works better with generics.
✅ 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 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
type 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: ...
def render(obj: Drawable) -> None:
obj.draw()
Dignified Python prefers ABC because it makes inheritance and intent explicit.
from typing import Self
class Stack[T]:
"""Type-safe stack with PEP 695 syntax."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> Self:
"""Push item and return self for chaining."""
self._items.append(item)
return self
def pop(self) -> T | None:
"""Pop item or return None if empty."""
if not self._items:
return None
return self._items.pop()
def peek(self) -> T | None:
"""Peek at top item without removing."""
if not self._items:
return None
return self._items[-1]
def is_empty(self) -> bool:
"""Check if stack is empty."""
return len(self._items) == 0
# Usage
numbers = Stack[int]()
numbers.push(1).push(2).push(3)
top = numbers.pop() # Type checker knows this is int | None
from abc import ABC, abstractmethod
from typing import Self
class Repository[T]:
"""Abstract repository with generic type parameter."""
@abstractmethod
def get(self, id: str) -> T | None:
"""Get entity by ID."""
@abstractmethod
def save(self, entity: T) -> Self:
"""Save entity, return self for chaining."""
@abstractmethod
def delete(self, id: str) -> bool:
"""Delete entity, return success."""
def get_or_fail(self, id: str) -> T:
"""Get entity or raise error."""
entity = self.get(id)
if entity is None:
raise ValueError(f"Entity not found: {id}")
return entity
class InMemoryRepository[T](Repository[T]):
"""In-memory repository implementation."""
def __init__(self) -> None:
self._storage: dict[str, T] = {}
def get(self, id: str) -> T | None:
return self._storage.get(id)
def save(self, entity: T) -> Self:
# Assume entity has 'id' attribute
entity_id = str(getattr(entity, "id", id(entity)))
self._storage[entity_id] = entity
return self
def delete(self, id: str) -> bool:
if id in self._storage:
del self._storage[id]
return True
return False
# Usage
from dataclasses import dataclass
@dataclass
class User:
id: str
name: str
repo = InMemoryRepository[User]()
repo.save(User("1", "Alice")).save(User("2", "Bob"))
user = repo.get("1") # Type: User | None
# Simple aliases
type UserId = str
type ErrorMessage = str
# Complex nested types
type JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
# Generic type aliases
type Result[T] = tuple[T, ErrorMessage | None]
type AsyncResult[T] = tuple[T | None, ErrorMessage | None]
def parse_int(value: str) -> Result[int]:
"""Parse string to int, return result with optional error."""
try:
return (int(value), None)
except ValueError as e:
return (0, str(e))
def fetch_user(id: UserId) -> AsyncResult[dict[str, str]]:
"""Fetch user data asynchronously."""
# Implementation...
return ({"id": id, "name": "Alice"}, None)
from typing import Self
class QueryBuilder[T]:
"""Generic query builder with fluent interface."""
def __init__(self, result_type: type[T]) -> None:
self._result_type = result_type
self._filters: list[str] = []
self._limit: int | None = None
def filter(self, condition: str) -> Self:
"""Add filter condition."""
self._filters.append(condition)
return self
def limit(self, n: int) -> Self:
"""Set result limit."""
self._limit = n
return self
def build(self) -> str:
"""Build query string."""
query = " AND ".join(self._filters)
if self._limit:
query += f" LIMIT {self._limit}"
return query
# Usage
@dataclass
class User:
name: str
age: int
builder = QueryBuilder[User](User)
query = (
builder
.filter("active = true")
.filter("age > 18")
.limit(10)
.build()
)
def map_list[T, U](items: list[T], func: Callable[[T], U]) -> list[U]:
"""Map function over list items."""
from collections.abc import Callable
return [func(item) for item in items]
def filter_list[T](items: list[T], predicate: Callable[[T], bool]) -> list[T]:
"""Filter list by predicate."""
from collections.abc import Callable
return [item for item in items if predicate(item)]
def reduce_list[T, U](
items: list[T],
func: Callable[[U, T], U],
initial: U,
) -> U:
"""Reduce list to single value."""
from collections.abc import Callable
result = initial
for item in items:
result = func(result, item)
return result
# Usage
numbers = [1, 2, 3, 4, 5]
doubled = map_list(numbers, lambda x: x * 2) # list[int]
evens = filter_list(numbers, lambda x: x % 2 == 0) # list[int]
sum_val = reduce_list(numbers, lambda acc, x: acc + x, 0) # int
✅ 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.12"
✅ 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[T](items: list[T], default: T) -> T:
if not items:
return default
return items[0]
Use PEP 695 for:
Still use TypeVar for:
TypeVar("T", str, bytes)If upgrading from Python 3.11:
Consider migrating to PEP 695 syntax:
TypeVar + def func(x: T) -> T → def func[T](x: T) -> TGeneric[T] + class C(Generic[T]) → class C[T]Consider using type statement for aliases:
Config = dict[str, str] → type Config = dict[str, str]Keep TypeVar for constraints:
TypeVar with constraints still neededAll existing 3.11 syntax continues to work:
Self type still preferred| still preferred