> ## Documentation Index
> Fetch the complete documentation index at: https://docs.refactron.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# pep604_optional_union

> Rewrite Optional[X] to X | None and Union[A, B] to A | B (PEP 604).

**Transform ID:** `pep604_optional_union`
**Language:** Python

## What it does

Rewrites the verbose `typing.Optional[X]` and `typing.Union[A, B, …]` annotations to the PEP 604 pipe syntax (`X | None`, `A | B`). Handles both the attribute form (`typing.Optional[…]`) and the bare form imported via `from typing import Optional, Union`. Collapses nested unions (`Union[A, Union[B, C]]` → `A | B | C`), unwraps single-arg unions (`Union[A]` → `A`), de-duplicates components, and moves `None` to the end for readability. The cleanup pass drops `Optional` / `Union` from `from typing import …` when no other references survive. Sidecar at `src/transform/transforms/python/_py/pep604_optional_union.py`.

## Detector pattern

The detector at `src/analyze/detectors/python/typing-optional-union.ts` matches `Subscript` nodes whose value is `Optional` or `Union` — either bare (after `from typing import …`) or attribute-accessed (`typing.Optional`, `<alias>.Union`).

## Preconditions

1. **`python_version_too_low`** — refuses when the resolved Python version is `< 3.10` **and** the file does not start with `from __future__ import annotations`. The pipe-union syntax (`A | B`) is a 3.10 runtime feature; under `__future__.annotations` it is valid as an annotation on 3.7+.
2. **`runtime_subscript_unsafe`** — refuses files that use `isinstance(x, Union[int, str])` on a `< 3.10` interpreter even with the `__future__` override. `int | str` is not a valid `isinstance` arg on Python `< 3.10`.
3. **`runtime_type_eval_unsafe`** — same conservative refusal as `pep585_generics`: Pydantic v1 or `typing.get_type_hints` without `__future__.annotations`. Pydantic v1 evaluates annotations at runtime; the pipe-union form may fail on older interpreters.
4. Only bare (or no-op-aliased) `from typing import Optional, Union` binds the canonical names we rewrite. Genuine renames are skipped.

## Before / after

<CodeGroup>
  ```python before.py theme={null}
  from typing import Optional, Union


  def lookup(key: str, default: Optional[int] = None) -> Union[int, str, None]:
      if key == "x":
          return 1
      return default if default is not None else "missing"
  ```

  ```python after.py theme={null}
  def lookup(key: str, default: int | None = None) -> int | str | None:
      if key == "x":
          return 1
      return default if default is not None else "missing"
  ```
</CodeGroup>

Nested / mixed unions collapse cleanly:

<CodeGroup>
  ```python before.py theme={null}
  from typing import Optional, Union

  X = Optional[Union[int, str]]
  Y = Union[int, Union[str, None]]
  ```

  ```python after.py theme={null}
  X = int | str | None
  Y = int | str | None
  ```
</CodeGroup>

## Edge cases NOT handled (skip via precondition)

* Pydantic v1 models or `get_type_hints` in the same file without `__future__.annotations` — `runtime_type_eval_unsafe`.
* Runtime `isinstance(x, Union[...])` on a `< 3.10` interpreter using the `__future__` override — `runtime_subscript_unsafe`.
* Aliased imports (`from typing import Optional as Opt`) — the alias rebinds the name; rewrite is skipped to avoid `NameError`s.
* `from typing import *` — every target name is conservatively assumed in scope but no cleanup is performed.

<Note>
  The 3.10 cutoff is one minor version newer than `pep585_generics` (3.9) because PEP 604's pipe-union runtime semantics landed later. Annotations-only use under `from __future__ import annotations` works on 3.7+.
</Note>
