Back to Blog
Python

Python Union Operator: `|` for Types and Sets

python union operator: Learn how the Python union operator `|` works for type hints and sets, including syntax, compatibility, and when to use `typing.Union`.

type hintsunion typesset operationsPEP 604typing
Illustration of the Python union operator combining two distinct shapes into a unified whole with a vertical bar.

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 directly support isinstance without extra work. 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 (without extra handling) | | 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, the runtime types.UnionType object is only available in 3.10+, so if you need to use the union type at runtime, you must stick to typing.Union for older versions.

from __future__ import annotations def process(value: int | str) -> None: # This works even on Python 3.7 with the future import pass

But if you need to do runtime checks with isinstance, you cannot use the future import alone. You would need to use typing.Union or manually handle the check.

Common Mistakes and Edge Cases

One common mistake is using | with non-type objects. 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 type 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 older versions or require runtime introspection that works with typing.Union, stick with typing.Union. For new projects targeting modern Python, | is the idiomatic choice.

python union operator: Practical Usage and Code Examples | RYUSLOG DEV