Optional, Union & the | Operator
Unions express values that may take multiple types. The X | Y syntax (3.10+) replaces Union[X, Y]; nullable types are unions with None.
Search across all documentation pages
| OperatorUnions express values that may take multiple types. The X | Y syntax (3.10+) replaces Union[X, Y]; nullable types are unions with None.
def parse_count(raw: str | None) -> int:
if raw is None:
return 0
return int(raw)
def format_id(value: int | str) -> str:
return str(value)When to reach for this:
from dataclasses import dataclass
@dataclass
class User:
id: int
email: str | None
def email_domain(user: User) -> str | None:
if user.email is None:
return None
return user.email.split("@", 1)[1]
def describe(value: int | str | None) -> str:
match value:
case None:
return "missing"
case int(n):
return f"number {n}"
case str(s):
return f"text {s}"
def coalesce(*values: str | None) -> str:
for v in values:
if v is not None:
return v
return ""
if __name__ == "__main__":
print(describe(42))
print(email_domain(User(1, None)))What this demonstrates:
is None / is not None narrows optional types for checkersmatch/case with typed patterns refines unionsstr | None in variadic args for fallback chainsNone explicit in return typeOptional[T] means T | None - still valid, | preferred style.--strict warns on unhandled union members in match.| Tool | Example |
|---|---|
is None | Optional -> T |
isinstance | Union members |
match/case | Structural + types |
assert | Checker-only hint |
from typing import TypeGuard
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)if email: treats empty string as missing differently from is None. Fix: Pick explicit None vs falsy semantics.T | None obscures required fields. Fix: Required vs optional model fields separated.| Alternative | Use When | Don't Use When |
|---|---|---|
Result/Either types | Explicit error channel | Exceptions idiomatic |
Literal | Fixed set of values | Open-ended types |
| exceptions | Truly exceptional missing | Expected optional field |
| sentinel object | Distinguish missing vs None | Simpler Optional suffices |
Same meaning - prefer str | None in modern Python 3.10+ codebases.
isinstance, match, equality to None, or TypeGuard functions.
isinstance(x, (int, str)) valid at runtime; checker narrows accordingly.
field: str | None = None or Optional default None.
int | None replaces Optional[int] - cleaner reading.
Include case _: or mypy may warn on unhandled union under strict settings.
User | Admin with shared Protocol or base class for common fields.
Pydantic Optional fields map null; document OpenAPI nullable carefully.
assert x is not None narrows for mypy - runtime no-op if optimization removes asserts (-O).
Refactor to base class, Protocol, or discriminated union with literal tag field.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 16, 2026