.agents/skills/dignified-python/versions/python-3.11.md
This document captures type annotation guidance for Python 3.11.
Python 3.11 builds on 3.10's type syntax with the addition of the Self type (PEP 673), making
method chaining and builder patterns significantly cleaner. All modern syntax from 3.10 continues to
work.
What's new in 3.11:
Self type for self-returning methods (PEP 673)Available from 3.10:
list[T], dict[K, V], etc. (PEP 585)| operator (PEP 604)X | NoneWhat you need from typing module:
Self for self-returning methods (NEW)TypeVar for generic functions/classesGeneric for generic 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] = []
✅ 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
# Usage with type safety
builder = Builder().set_name("app").set_value(42)
❌ WRONG - Don't use bound TypeVar anymore:
from typing import TypeVar
T = TypeVar("T", bound="Builder")
class Builder:
def set_name(self: T, name: str) -> T: # Don't do this
...
When to use Self:
selfSelf in classmethod:
from typing import Self
class Config:
def __init__(self, data: dict[str, str]) -> None:
self.data = data
@classmethod
def from_file(cls, path: str) -> Self:
"""Load config from file."""
import json
with open(path, encoding="utf-8") as f:
data = json.load(f)
return cls(data)
✅ 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 value
Note: Python 3.12 introduces better syntax (PEP 695) for this pattern.
✅ 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) -> Self: # Can combine with 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) # Method chaining works!
Note: Python 3.12 introduces cleaner syntax for generic classes.
✅ 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 typing import Self
class QueryBuilder:
"""SQL query builder with fluent interface."""
def __init__(self) -> None:
self._select: list[str] = ["*"]
self._from: str | None = None
self._where: list[str] = []
self._limit: int | None = None
def select(self, *columns: str) -> Self:
"""Specify columns to select."""
self._select = list(columns)
return self
def from_table(self, table: str) -> Self:
"""Specify table to query."""
self._from = table
return self
def where(self, condition: str) -> Self:
"""Add WHERE condition."""
self._where.append(condition)
return self
def limit(self, n: int) -> Self:
"""Set LIMIT."""
self._limit = n
return self
def build(self) -> str:
"""Build final SQL query."""
if not self._from:
raise ValueError("FROM table not specified")
parts = [f"SELECT {', '.join(self._select)}"]
parts.append(f"FROM {self._from}")
if self._where:
parts.append(f"WHERE {' AND '.join(self._where)}")
if self._limit:
parts.append(f"LIMIT {self._limit}")
return " ".join(parts)
# Usage with type-safe method chaining
query = (
QueryBuilder()
.select("id", "name", "email")
.from_table("users")
.where("active = true")
.where("age > 18")
.limit(10)
.build()
)
from typing import Self
from pathlib import Path
import json
class Config:
"""Application configuration with multiple factory methods."""
def __init__(self, data: dict[str, str | int]) -> None:
self.data = data
@classmethod
def from_json(cls, path: Path) -> Self:
"""Load configuration from JSON file."""
if not path.exists():
raise FileNotFoundError(f"Config not found: {path}")
with path.open(encoding="utf-8") as f:
data = json.load(f)
return cls(data)
@classmethod
def from_env(cls) -> Self:
"""Load configuration from environment variables."""
import os
data = {
k.lower(): v
for k, v in os.environ.items()
if k.startswith("APP_")
}
return cls(data)
@classmethod
def default(cls) -> Self:
"""Create default configuration."""
return cls({"host": "localhost", "port": 8080})
def with_override(self, key: str, value: str | int) -> Self:
"""Return new config with overridden value."""
new_data = self.data.copy()
new_data[key] = value
return type(self)(new_data)
# All factory methods return correct type
config = Config.from_json(Path("config.json"))
dev_config = config.with_override("debug", True)
✅ 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.11"
✅ 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.10:
Replace bound TypeVar with Self for self-returning methods:
T = TypeVar("T", bound="ClassName")from typing import Self and use -> SelfEnjoy improved error messages (no code changes needed)
All existing 3.10 syntax continues to work