Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

Python Union Types with |

Learn how Python combines types with the | union expression, includes None as an allowed type, simplifies equivalent unions, and uses supported unions in runtime instance and subclass checks.

Levels: Intermediate ~18 min read

Intermediate

A union type describes a value that may belong to one of several types. Python writes a union with the | operator between type objects. This lesson focuses on union expressions themselves and their documented runtime behavior, building on the existing lessons about annotations and type aliases.

Write a union with |

For ordinary built-in type objects, int | str creates a union meaning “an integer or a string.” Use it in an annotation when either kind of value is acceptable for the function’s contract.

python union-basics.py
1
2
3
4
5
def display_code(code: int | str) -> str:
    return f"Code: {code}"

print(display_code(42))
print(display_code("A-17"))

The same function signature can describe either an integer code or a string code.

Quick check

What does the annotation int | str express?

def label(value: int | str) -> str:
    return str(value)

print(label(6))

Include None as one possibility

A return value can use str | None when a result may be a string or may be absent. This expresses two possible types; it is not the same as saying a function argument is optional because it has a default value.

python union-with-none.py
1
2
3
4
5
6
def find_label(code: int) -> str | None:
    labels = {7: "ready", 9: "waiting"}
    return labels.get(code)

print(find_label(7))
print(find_label(3))

None is included as a member of the union. The dictionary lookup supplies either a string or None.

Quick check

Which expression is equivalent to Optional[str]?

def find_title(item_id: int) -> str | None:
    return "Guide" if item_id == 1 else None

print(find_title(2))

Recognize equivalent unions

Union expressions discard redundant member types and compare equal regardless of member order. Nested unions are flattened for ordinary type expressions, so adding an already-listed type does not create a different union.

python union-equivalence.py
1
2
3
4
5
6
first = int | str | int
second = str | int
third = (int | str) | float

print(first == second)
print(third == int | str | float)

Equivalent type unions compare equal even when the order differs or a member is repeated.

Quick check

What does int | str | int == str | int evaluate to?

print((int | str | int) == (str | int))

Use a union in runtime checks

Python also accepts a union of runtime-checkable types in isinstance() and issubclass(). These checks ask whether an object or class matches at least one union member; they do not perform the job of a static type checker.

python union-runtime-checks.py
1
2
3
4
5
6
def is_simple_value(value: object) -> bool:
    return isinstance(value, int | str)

print(is_simple_value("Foxipa"))
print(is_simple_value(3.5))
print(issubclass(bool, int | float))

The runtime check succeeds when at least one acceptable union member matches; bool is a subclass of int.

Quick check

Which runtime check asks whether a value matches at least one member of int | str?

print(isinstance("7", int | str))

Know the limits of runtime checks

A string forward reference is another boundary: writing int | "LaterClass" does not construct a union at runtime because one operand is a string, not a type object. When a forward reference is needed, quote the complete annotation expression, for example "int | LaterClass".

A type expression can be useful in an annotation even when it is not valid as a runtime isinstance() check. Parameterized generic aliases such as list[int] are not valid runtime class-info entries. Do not depend on short-circuit evaluation to hide an invalid member; keep every member valid for the runtime check you are making.

python union-runtime-limit.py
1
2
3
4
try:
    print(isinstance([1, 2], str | list[int]))
except TypeError as error:
    print(type(error).__name__)

A parameterized generic such as list[int] cannot be used as a runtime class-info entry in this union check.

Quick check

Why does isinstance([1], str | list[int]) raise TypeError?

try:
    isinstance([1], str | list[int])
except TypeError as error:
    print(type(error).__name__)

If a union expression becomes long or appears repeatedly, give the expression a meaningful type-alias name using the separate Type Alias Statements lesson. That lesson owns alias syntax; this lesson owns the union expression and its behavior.

Key idea

Use X | Y to express a union of types. Include None when absence is a valid possibility, rely on normalization rather than member ordering, and use runtime checks only when every union member is valid for isinstance() or issubclass().

Key takeaways

  • A union expression X | Y says that a value may match either listed type.
  • str | None expresses a string or None; it is equivalent to Optional[str].
  • Repeated union members are removed for equivalence, and member order does not change union equality.
  • isinstance() and issubclass() accept unions when their members are valid runtime class-info objects.
  • Parameterized generics such as list[int] are not valid runtime class-info entries.
Was this page helpful?

Mark this learning resource complete to track your learning progress.