# What is PEP 604?


[PEP 604: Allow writing union types as X | Y](https://peps.python.org/pep-0604/) 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](https://pydevtools.com/handbook/explanation/what-is-pep-484.md) 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:

```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:

```python
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](https://peps.python.org/pep-0563/), the older deferred-evaluation mechanism), the interpreter stores the text without computing it. From Python 3.14, [PEP 649](https://pydevtools.com/handbook/explanation/what-is-pep-649.md) 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](https://pydevtools.com/handbook/reference/ruff.md) 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.

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

## Learn More

- [PEP 604: Allow writing union types as X | Y](https://peps.python.org/pep-0604/)
- [Ruff rule UP007: non-pep604-annotation-union](https://docs.astral.sh/ruff/rules/non-pep604-annotation-union/)
- [Ruff rule UP045: non-pep604-annotation-optional](https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional/)
- [What is PEP 695?](https://pydevtools.com/handbook/explanation/what-is-pep-695.md) covers the matching modernization for generics
