Subchapter 2.14
versions/python-3.13.mdMarkdown14 KBView on GitHub
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 implementationNote: 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 validNote: 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 TruePrefer 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.13Configuration (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 resultIf 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