Python Union Operator: `|` for Types and Sets
Learn how Python's `|` operator works for type hints and set unions, including syntax, runtime behavior, compatibility, and when to use `typing.Union`.
The Python union operator, written as |, is a versatile symbol that appears in two common contexts: type hints and set operations. Understanding how it behaves in each context is essential for writing correct, maintainable code.
The | Operator in Type Hints
In Python 3.10, the | operator became a valid way to express union types in type hints. Instead of writing Union[int, str], you can write int | str. This is a more readable and concise syntax for the same concept.
def process(value: int | str) -> None: print(value)
This function accepts either an integer or a string. The type checker understands that value is of type int | str, meaning it can be either.
The operator works with any types, including classes, None, and other type constructs. For example, int | None is equivalent to Optional[int].
How | Works at Runtime
The | operator for types is not just a syntax trick. At runtime, int | str actually creates a types.UnionType object. This object supports isinstance checks, so you can use it in runtime type validation.
type_union = int | str print(type_union) # int | str print(isinstance(42, type_union)) # True
This is a change from typing.Union, which returns a _UnionGenericAlias and does not support isinstance; for runtime checks you need a tuple of types, such as isinstance(value, (int, str)). The new syntax is more aligned with runtime behavior.
Set Union with |
The | operator has long been used for set union. For two sets, a | b returns a new set containing all elements from both sets.
a = {1, 2, 3} b = {3, 4, 5} print(a | b) # {1, 2, 3, 4, 5}
This is a separate feature from type unions, but the same operator symbol is used. The context determines the meaning: when applied to types, it creates a union type; when applied to sets, it performs a set union.
Comparing | and typing.Union
Before Python 3.10, the standard way to express union types was typing.Union. The new | syntax is more concise and often preferred in new code, but there are compatibility considerations.
| Feature | int | str | typing.Union[int, str] |
|---|---|---|
| Python version | 3.10+ | 3.5+ |
| Runtime object | types.UnionType | _UnionGenericAlias |
Supports isinstance | Yes | No |
| Readability | Concise and natural | Verbose but explicit |
| Forward references | Requires from __future__ import annotations in earlier versions | Same |
For code that must run on Python 3.9 or earlier, typing.Union remains the safe choice. If you are using Python 3.10 or later, the | syntax is generally recommended for new code.
Compatibility and Migration
If you are working on a codebase that needs to support multiple Python versions, you have a few options. The from __future__ import annotations import postpones evaluation of annotations, so you can use int | str even on Python 3.7 and 3.8, as long as the type checker and runtime tools support it. However, because annotations are stored as strings, they are not available as live objects at runtime. The runtime types.UnionType object is only available in 3.10+, so if you need to use the union type at runtime, you must use Python 3.10+ or fall back to regular tuple-based checks.
from __future__ import annotations def process(value: int | str) -> None: # This annotation is not evaluated at definition time, # so this works even on Python 3.7 with the future import. pass
But if you need to do runtime checks with isinstance, the future import alone does not help. On Python 3.10+, use isinstance(value, int | str). On older versions, use a tuple of types, such as isinstance(value, (int, str)); typing.Union does not provide runtime isinstance support.
Common Mistakes and Edge Cases
One common point of confusion is the placement of | in complex types. For example, list[int] | list[str] works, but list[int | str] is different: it means a list where each element is either an int or a string, not a list that is either all ints or all strings. The placement of the operator matters.
Another edge case is the interaction with None. int | None is equivalent to Optional[int], but some developers mistakenly write int or None in type hints, which does not work. The | operator is the correct syntax.
Also, note that | has lower precedence than [] in Python expressions, so list[int] | list[str] is parsed as (list[int]) | (list[str]), which is correct. But list[int | str] is a list of a union type.
When to Use | vs typing.Union
The decision largely depends on your Python version and runtime needs. If you are on Python 3.10+ and want concise, readable type hints, use |. If you need to support Python 3.9 or earlier, or you are relying on tooling that has not implemented the new syntax, typing.Union remains the compatible choice. For new projects targeting modern Python, | is the idiomatic choice.