How to Use typing.Optional vs Union in Python 3.10+
Python 3.10 introduced PEP 604, enabling native union syntax with the | operator. This guide details the exact migration path from typing.Union and typing.Optional to modern equivalents. You will ensure compatibility with static analyzers like mypy and pyright. For foundational concepts, review Core Type Hints Fundamentals before implementing syntax changes.
Understanding the semantic equivalence between legacy imports and native operators is critical. It ensures consistent Union and Optional Types across enterprise codebases. Static type checkers treat both forms identically post-3.10. Runtime checks require specific handling.
The PEP 604 Syntax Shift: T | None vs Optional[T]
typing.Optional[T] compiles internally to typing.Union[T, None]. The native T | None syntax reduces import overhead. It also improves readability without altering runtime performance. Modern Python codebases should standardize on the pipe operator.
from typing import Optional, Union
# Legacy syntax
def process_legacy(data: Optional[Union[str, int]]) -> None:
pass
# Python 3.10+ syntax
def process_modern(data: str | int | None) -> None:
pass
The modern signature is strictly equivalent. Type checkers resolve both to identical abstract syntax trees. You can safely remove redundant typing imports.
Static Analysis & Runtime Type Checking Compatibility
mypy and pyright normalize both syntaxes to the same internal representation. This guarantees zero false positives in CI pipelines. However, runtime isinstance checks behave differently.
from types import UnionType
def validate_runtime(val: int | str) -> bool:
# Python 3.10+ supports native union type checks at runtime
return isinstance(type(val), UnionType) or isinstance(val, (int, str))
Legacy typing.get_union_args() is deprecated. You must use typing.get_args() for runtime introspection. The standard library flattens nested unions automatically.
import typing
alias = int | str | None
print(typing.get_args(alias))
# Output: (<class 'int'>, <class 'str'>, <class 'NoneType'>)
pyright enforces stricter union flattening rules than mypy. Always verify runtime behavior separately from static analysis.
Step-by-Step Migration for Large Codebases
Automate bulk conversions using pyupgrade. Run the tool against your target Python version to rewrite signatures safely.
pyupgrade --py310-plus **/*.py
Configure Ruff to flag legacy imports automatically. Add this to your pyproject.toml or ruff.toml.
[tool.ruff.lint]
select = ["UP007"] # Enforces PEP 604 union syntax
Validate your pipeline with strict mypy mode. This catches residual type mismatches early.
mypy --strict --python-version 3.10 src/
Note that __future__ annotations enable | syntax in Python 3.7+. Runtime isinstance checks still require 3.10+. Isolate static hints from runtime logic when supporting older interpreters.
Edge Cases: Forward References and typing.get_args
String forward references parse identically under PEP 604. You can safely use "T | None" in recursive type aliases. typing.get_args() returns flattened tuples for nested unions.
Avoid mixing Union and | in the same signature. Static analyzers flag inconsistent syntax as a style violation. Standardize on | for all new code and legacy refactors.
# Safe forward reference
def recursive(data: "Node | None") -> "Node | None":
...
Complex generics may trigger false positives in older mypy versions. Upgrade to mypy>=1.0 to resolve nested union evaluation bugs.
Common Mistakes
- Assuming
typing.Optionalis deprecated: It remains fully supported for backward compatibility. Using it adds unnecessary verbosity in 3.10+. - Mixing
typing.Unionand|in the same signature: Static analyzers flag inconsistent union syntax as a style violation. Standardize on|. - Using
isinstance(x, typing.Union)for runtime checks:typing.Unionis a generic alias. Python 3.10+ requirestypes.UnionTypeor direct tuple checks.
FAQ
Is typing.Optional officially deprecated in Python 3.10+?
No. It remains fully supported for backward compatibility. PEP 604 recommends T | None for cleaner syntax.
How does mypy handle X | Y vs Union[X, Y] internally?
mypy normalizes both to an identical internal UnionType representation. Static analysis yields identical error messages.
Can I use the | operator in Python 3.9 with __future__?
Yes. from __future__ import annotations enables PEP 604 syntax in 3.7+ for type hints. Runtime isinstance checks still require 3.10+.