Python typing.Union vs | Operator Explained
This article compares Python’s traditional typing.Union
with the modern bitwise OR (|) union operator syntax
introduced in PEP 604. While both approaches allow developers to declare
that a variable, argument, or return value can accept multiple types,
they differ significantly in terms of readability, Python version
compatibility, runtime behavior, and import overhead. Modern Python
favors the cleaner | syntax, rendering
typing.Union largely legacy for newer projects.
Syntax and Imports
The most immediate difference is how each approach is written. Prior
to Python 3.10, specifying that a value could be an integer or a string
required importing Union from the standard
typing module:
from typing import Union
def parse_data(value: Union[int, str]) -> Union[int, float]:
...Starting in Python 3.10, the bitwise OR operator (|) can
be used directly between types without requiring an import:
def parse_data(value: int | str) -> int | float:
...The pipe syntax also simplifies nullable types. Instead of importing
Optional or writing Union[str, None], you can
write str | None, which is shorter and immediately explicit
about allowing None.
Version Compatibility
Compatibility is the primary constraint when choosing between the two syntaxes:
typing.Union: Supported in Python 3.5 and all subsequent versions. It remains necessary if your codebase must execute on Python 3.9 or older.|syntax: Fully supported natively in Python 3.10 and later.
If you are running Python 3.7 through 3.9, you can still use the
| syntax strictly for type annotations by adding
from __future__ import annotations at the top of the file.
This delays the evaluation of type annotations, allowing static type
checkers (like Mypy or Pyright) to parse the pipe syntax. However,
runtime evaluation will still fail on these older versions.
Runtime Behavior and Type Checking
Beyond static analysis, the | operator integrates
directly into standard Python runtime operations where
typing.Union fails.
In Python 3.10+, union expressions can be passed directly to
isinstance() and issubclass():
# Valid in Python 3.10+
isinstance(42, int | str) # Returns True
issubclass(bool, int | str) # Returns TrueIn contrast, passing typing.Union directly to
isinstance() raises a TypeError:
from typing import Union
# Raises TypeError: Cannot use Union to instantiate or check instances
isinstance(42, Union[int, str])Under the hood, int | str evaluates to an instance of
types.UnionType, whereas Union[int, str]
creates a typing._UnionGenericAlias. Static type checkers
treat both identically, but types.UnionType is a lighter,
built-in construct.
Recommendation
For any project running exclusively on Python 3.10 or newer, use the
| operator syntax. It eliminates unnecessary imports,
produces more readable function signatures, and provides native runtime
support with functions like isinstance(). Reserve
typing.Union solely for projects that must maintain
backwards compatibility with Python 3.9 and earlier without the use of
postponed evaluation of annotations.