PEP 585 Generics
TL;DR
PEP 585 made built-in collections generic, allowing list[int]
instead of List[int]
- eliminating duplicate type hierarchies in the typing module.
Interesting!
Before PEP 585, Python had two separate type systems - one for runtime and one for type hints. Now built-in collections work for both!
Before and After
python code snippet start
# Old way (before Python 3.9)
from typing import List, Dict, Set, Tuple, Optional
def process_data(
items: List[str],
mapping: Dict[str, int],
unique_values: Set[float],
coordinates: Tuple[int, int],
config: Optional[Dict[str, str]]
) -> List[Dict[str, int]]:
pass
# New way (Python 3.9+)
def process_data(
items: list[str],
mapping: dict[str, int],
unique_values: set[float],
coordinates: tuple[int, int],
config: dict[str, str] | None
) -> list[dict[str, int]]:
pass
python code snippet end
All Generic Collections
python code snippet start
# Basic collections
numbers: list[int] = [1, 2, 3]
word_count: dict[str, int] = {"hello": 5, "world": 5}
unique_ids: set[str] = {"abc", "def", "ghi"}
point: tuple[float, float] = (3.14, 2.71)
# Nested generics
matrix: list[list[int]] = [[1, 2], [3, 4]]
user_groups: dict[str, list[str]] = {
"admins": ["alice", "bob"],
"users": ["charlie", "diana"]
}
# Complex nesting
api_response: dict[str, list[dict[str, int | str]]] = {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
}
python code snippet end
Collections Module Generics
python code snippet start
from collections import defaultdict, deque, Counter
from collections.abc import Mapping, Sequence
# collections types are now generic too
word_lists: defaultdict[str, list[str]] = defaultdict(list)
task_queue: deque[str] = deque(["task1", "task2"])
letter_counts: Counter[str] = Counter("hello world")
# Abstract base classes
def process_mapping(data: Mapping[str, int]) -> None:
for key, value in data.items():
print(f"{key}: {value}")
def process_sequence(items: Sequence[str]) -> str:
return ", ".join(items)
python code snippet end
Runtime Introspection
python code snippet start
# Type information is preserved at runtime
def analyze_type(obj):
print(f"Type: {type(obj)}")
if hasattr(type(obj), '__args__'):
print(f"Generic args: {type(obj).__args__}")
if hasattr(type(obj), '__origin__'):
print(f"Origin: {type(obj).__origin__}")
# Create parameterized types
string_list = list[str]
int_dict = dict[str, int]
print(string_list) # <class 'list[str]'>
print(int_dict) # <class 'dict[str, int]'>
# Runtime type checking
def validate_list(items, expected_type):
if not isinstance(items, list):
return False
# Note: Can't check generic parameters at runtime with isinstance
return True
numbers = [1, 2, 3]
validate_list(numbers, list[int]) # True (but doesn't check int type)
python code snippet end
Gradual Migration
python code snippet start
# Support both old and new styles during transition
from typing import List, Dict # Still works
# You can mix and match during migration
def legacy_function(old_style: List[str]) -> dict[str, int]:
result: dict[str, int] = {}
for item in old_style:
result[item] = len(item)
return result
# Future annotations for earlier Python versions
from __future__ import annotations
def forward_compatible(items: list[str]) -> dict[str, int]:
# This syntax works even in Python 3.7+ with __future__ import
return {item: len(item) for item in items}
python code snippet end
Real-World Usage
python code snippet start
def load_config() -> dict[str, str | int | bool]:
"""Load configuration with mixed value types."""
return {
"host": "localhost",
"port": 8080,
"debug": True,
"max_connections": 100
}
def process_api_data(
response: dict[str, list[dict[str, str | int]]]
) -> list[tuple[str, int]]:
"""Extract user data from API response."""
users = response.get("users", [])
return [(user["name"], user["id"]) for user in users]
def cache_results(
cache: dict[str, str],
computations: list[tuple[str, callable]]
) -> None:
"""Cache expensive computation results."""
for key, func in computations:
if key not in cache:
cache[key] = str(func())
# Usage with proper type hints
config = load_config()
api_data = {"users": [{"name": "Alice", "id": 1}]}
results = process_api_data(api_data)
cache: dict[str, str] = {}
computations: list[tuple[str, callable]] = [
("pi", lambda: 3.14159),
("answer", lambda: 42)
]
cache_results(cache, computations)
python code snippet end
Type Checking Benefits
python code snippet start
# Type checkers can now provide better error detection
def sum_numbers(values: list[int]) -> int:
return sum(values)
# Type checker will catch this error:
# sum_numbers(["1", "2", "3"]) # Error: Expected list[int], got list[str]
# Correct usage:
sum_numbers([1, 2, 3]) # OK
# IDE autocompletion works better
user_data: dict[str, str] = {"name": "Alice"}
# IDE knows .keys() returns dict_keys[str]
# IDE knows .values() returns dict_values[str]
python code snippet end
PEP 585 eliminated the artificial separation between runtime types and type hints - making Python’s type system more intuitive and reducing import overhead!
Reference: PEP 585 – Type Hinting Generics In Standard Collections