PEP 604 Union Types
TL;DR
PEP 604 introduced the |
operator for union types, allowing int | str
instead of Union[int, str]
for cleaner type annotations.
Interesting!
The |
operator for union types makes Python’s type hints look more like mathematical set notation - int | str
means “integer OR string”!
Before and After
python code snippet start
# Old verbose syntax
from typing import Union, Optional, List
def process_data(value: Union[int, str],
items: List[Union[int, float]],
config: Optional[dict]) -> Union[str, None]:
pass
# New concise syntax (Python 3.10+)
def process_data(value: int | str,
items: list[int | float],
config: dict | None) -> str | None:
pass
python code snippet end
Basic Union Types
python code snippet start
# Simple unions
def format_value(data: int | str | float) -> str:
return str(data)
# Optional is just union with None
def get_user(user_id: int) -> dict | None:
if user_id in users:
return users[user_id]
return None
# Multiple types in collections
def process_mixed_list(items: list[str | int | bool]) -> None:
for item in items:
if isinstance(item, str):
print(f"String: {item}")
elif isinstance(item, int):
print(f"Number: {item}")
elif isinstance(item, bool):
print(f"Boolean: {item}")
python code snippet end
Runtime Type Checking
python code snippet start
# Union types work with isinstance()
number_or_string = int | str
def validate_input(value):
if isinstance(value, number_or_string):
print("Valid input!")
return True
else:
print("Invalid input!")
return False
validate_input(42) # Valid input!
validate_input("hello") # Valid input!
validate_input([1, 2]) # Invalid input!
python code snippet end
Complex Nested Unions
python code snippet start
# Nested data structures with unions
from typing import TypeVar
T = TypeVar('T')
def process_response(data: dict[str, int | str | list[str]] | list[dict] | None) -> bool:
if data is None:
return False
if isinstance(data, dict):
# Process dictionary response
return all(isinstance(v, (int, str, list)) for v in data.values())
if isinstance(data, list):
# Process list of dictionaries
return all(isinstance(item, dict) for item in data)
return False
python code snippet end
Function Overloading with Unions
python code snippet start
def serialize(obj: dict | list | str | int | float | bool | None) -> str:
"""Serialize various Python objects to JSON-compatible strings."""
import json
if obj is None:
return "null"
elif isinstance(obj, bool):
return "true" if obj else "false"
elif isinstance(obj, (int, float)):
return str(obj)
elif isinstance(obj, str):
return json.dumps(obj) # Properly escape string
elif isinstance(obj, (dict, list)):
return json.dumps(obj)
else:
raise TypeError(f"Cannot serialize type {type(obj)}")
# Usage
print(serialize({"name": "Alice", "age": 30})) # {"name": "Alice", "age": 30}
print(serialize([1, 2, 3])) # [1, 2, 3]
print(serialize("hello")) # "hello"
print(serialize(None)) # null
python code snippet end
Backwards Compatibility
python code snippet start
# Code works with both syntaxes for gradual migration
from typing import Union
# Old style (still works)
def old_style(x: Union[int, str]) -> str:
return str(x)
# New style (Python 3.10+)
def new_style(x: int | str) -> str:
return str(x)
# Both are equivalent at runtime
assert old_style.__annotations__ == {'x': Union[int, str], 'return': str}
# new_style annotations are equivalent but use the | operator type
python code snippet end
Type Alias Definitions
python code snippet start
# Create reusable type aliases
NumberType = int | float | complex
StringOrBytes = str | bytes
JSONValue = dict | list | str | int | float | bool | None
def process_json(data: JSONValue) -> str:
if isinstance(data, dict):
return f"Object with {len(data)} keys"
elif isinstance(data, list):
return f"Array with {len(data)} items"
elif isinstance(data, str):
return f"String: {data}"
elif isinstance(data, (int, float)):
return f"Number: {data}"
elif isinstance(data, bool):
return f"Boolean: {data}"
else:
return "null"
# Use the alias
config: dict[str, NumberType | str] = {
"timeout": 30,
"host": "localhost",
"port": 8080,
"debug": True
}
python code snippet end
Practical API Design
python code snippet start
from pathlib import Path
def read_config(source: str | Path | dict) -> dict:
"""Read configuration from various sources."""
if isinstance(source, str):
# String path
with open(source) as f:
import json
return json.load(f)
elif isinstance(source, Path):
# Path object
return json.loads(source.read_text())
elif isinstance(source, dict):
# Already a dictionary
return source.copy()
else:
raise TypeError("Config source must be string, Path, or dict")
# Flexible usage
config1 = read_config("config.json")
config2 = read_config(Path("config.json"))
config3 = read_config({"debug": True, "port": 8080})
python code snippet end
Union types with the |
operator make type hints more readable and intuitive - bringing Python’s type system closer to how we naturally think about data!
Reference: PEP 604 – Allow writing union types as X | Y