.agents/skills/dignified-python/versions/python-3.13.md
This document captures type annotation guidance for Python 3.13. Python 3.13 implements PEP 649 (Deferred Evaluation of Annotations), fundamentally changing how annotations are evaluated.
The key change: forward references and circular imports work naturally without
from __future__ import annotations.
All type features from previous versions (3.10-3.12) continue to work.
What's new in 3.13:
from __future__)from __future__ import annotationsAvailable from 3.12:
def func[T](x: T) -> Ttype statement for better type aliasesAvailable from 3.11:
Self type for self-returning methodsCode Clarity:
IDE Support:
Bug Prevention:
All public APIs:
self and cls)Internal code:
count = 0)ā 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] = []
Why: Built-in types are more concise, don't require imports, and are the modern Python standard (available since 3.10).
ā
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 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
ā 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.
ā 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.
ā CORRECT - Just works naturally with PEP 649:
# Forward reference - no quotes needed!
class Node:
def __init__(self, value: int, parent: Node | None = None):
self.value = value
self.parent = parent
# Circular imports - just works!
# a.py
from b import B
class A:
def method(self) -> B:
...
# b.py
from a import A
class B:
def method(self) -> A:
...
# Recursive types - no future needed!
type JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
ā WRONG - Don't use from __future__ import annotations:
from __future__ import annotations # DON'T DO THIS in Python 3.13
class Node:
def __init__(self, value: int, parent: Node | None = None):
...
Why avoid from __future__ import annotations in 3.13:
from typing import Self
from collections.abc import Callable
class Node[T]:
"""Tree node - forward reference works naturally in 3.13!"""
def __init__(
self,
value: T,
parent: Node[T] | None = None, # Forward ref, no quotes!
children: list[Node[T]] | None = None, # Forward ref, no quotes!
) -> None:
self.value = value
self.parent = parent
self.children = children or []
def add_child(self, child: Node[T]) -> Self:
"""Add child and return self for chaining."""
self.children.append(child)
child.parent = self
return self
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 - all type-safe with no __future__ import!
root = Node[int](1)
root.add_child(Node[int](2)).add_child(Node[int](3))
from abc import ABC, abstractmethod
from typing import Self
class Entity[T]:
"""Base class for entities."""
def __init__(self, id: T) -> None:
self.id = id
class Repository[T](ABC):
"""Generic repository interface."""
@abstractmethod
def get(self, id: str) -> T | None:
"""Get entity by ID."""
@abstractmethod
def save(self, entity: T) -> None:
"""Save entity."""
@abstractmethod
def delete(self, id: str) -> bool:
"""Delete entity, return True if deleted."""
class User(Entity[str]):
def __init__(self, id: str, name: str) -> None:
super().__init__(id)
self.name = name
class UserRepository(Repository[User]):
def __init__(self) -> None:
self._users: dict[str, User] = {}
def get(self, id: str) -> User | None:
if id not in self._users:
return None
return self._users[id]
def save(self, entity: User) -> None:
self._users[entity.id] = entity
def delete(self, id: str) -> bool:
if id not in self._users:
return False
del self._users[id]
return True
Prefer specificity:
# ā
GOOD - Specific
def get_config() -> dict[str, str | int]:
...
# ā WRONG - Too vague
def get_config() -> dict:
...
Use Union sparingly:
# ā
GOOD - Union only when necessary
def process(value: str | int) -> str:
...
# ā WRONG - Too permissive
def process(value: str | int | list | dict) -> str | None | list:
...
Be explicit with None:
# ā
GOOD - Explicit optional
def find_user(id: str) -> User | None:
...
# ā WRONG - Implicit None return
def find_user(id: str) -> User:
return None # Type checker error!
Avoid Any when possible:
# ā
GOOD - Specific type
def serialize(obj: User | Config) -> str:
...
# ā WRONG - Defeats purpose of types
from typing import Any
def serialize(obj: Any) -> str:
...
Always type:
Type when helpful:
Can skip:
count = 0, name = "example"Dignified Python uses ty for static type checking:
# Check all files
ty check
# Check specific file
ty check src/mymodule.py
# Check with specific Python version
ty check --python-version 3.13
Configuration (in pyproject.toml):
[tool.ty.environment]
python-version = "3.13"
ā Don't ignore type errors with # type: ignore
# ā WRONG - Hiding type error
result = unsafe_function() # type: ignore
# ā
CORRECT - Fix the type error
result: Expected = cast(Expected, unsafe_function())
ā Don't use bare Exception in type hints
# ā WRONG - No value from typing exception
def risky() -> str | Exception:
...
# ā
CORRECT - Let exceptions bubble
def risky() -> str:
... # Raises ValueError on error
ā Don't over-type simple cases
# ā WRONG - Obvious from context
def add_numbers(a: int, b: int) -> int:
result: int = a + b # Unnecessary type annotation
return result
# ā
CORRECT - Type only signature
def add_numbers(a: int, b: int) -> int:
result = a + b # Type is obvious
return result
If migrating from Python 3.10/3.11:
from __future__ import annotations - No longer neededtype statement for aliases - More explicit than assignment# Python 3.10/3.11
from __future__ import annotations
from typing import TypeVar, Generic
T = TypeVar("T")
class Node(Generic[T]):
def __init__(self, value: T, parent: "Node[T] | None" = None):
...
# Python 3.13
from typing import Self
class Node[T]:
def __init__(self, value: T, parent: Node[T] | None = None):
...
Very rare:
TypeVar - Only for constrained/bounded type variablesAny - Use sparingly when type truly unknownProtocol - Structural typing (prefer ABC)TYPE_CHECKING - Conditional imports to avoid circular dependenciesNever needed:
List, Dict, Set, Tuple - Use built-in typesUnion - Use | operatorOptional - Use X | NoneGeneric - Use PEP 695 class syntax