Skip to content

What is PEP 604?

PEP 604: Allow writing union types as X | Y lets Python 3.10 and later spell a union of types with the pipe operator. int | str replaces Union[int, str], and int | None replaces Optional[int], with no typing import required.

This is part of the broader cleanup of PEP 484 type-hint syntax that moved typing constructs out of the typing module and into the language.

What changed?

Annotations that needed two imports now read as plain Python:

from typing import Optional, Union

def parse(raw: Union[str, bytes], default: Optional[int] = None) -> Union[int, None]: ...

# becomes
def parse(raw: str | bytes, default: int | None = None) -> int | None: ...

The operator also works at runtime. int | str evaluates to a types.UnionType object, and isinstance() and issubclass() accept it directly:

isinstance(1, int | str)   # True on Python 3.10+

Which Python versions support it?

The version requirement depends on where the union is evaluated, and this trips people up:

  • In annotations, on Python 3.10+, int | None always works.
  • In annotations on 3.8 and 3.9, it works only if the annotation is never evaluated: with from __future__ import annotations (string annotations, from PEP 563, the older deferred-evaluation mechanism), the interpreter stores the text without computing it. From Python 3.14, PEP 649 defers evaluation by default.
  • Anywhere the union is evaluated as a value, you need 3.10+. That includes isinstance() checks and libraries like Pydantic that resolve annotations at runtime to build validators.

So from __future__ import annotations lets a library targeting 3.9 use the new syntax in signatures, but the same union written inside isinstance() raises TypeError: unsupported operand type(s) on 3.9. If your code or your dependencies inspect annotations at runtime, treat 3.10 as the real floor.

Let Ruff rewrite your unions

Ruff automates the migration with two rules: UP007 (non-pep604-annotation-union) rewrites Union[X, Y] to X | Y, and UP045 (non-pep604-annotation-optional) rewrites Optional[X] to X | None. Both offer automatic fixes and respect your project’s target-version, so they won’t rewrite code that still supports 3.9 without the future import.

[tool.ruff.lint]
extend-select = ["UP007", "UP045"]

Learn More

Last updated on