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:

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 True

In 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.