Skip to main content

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.

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

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"
Nested / mixed unions collapse cleanly:
from typing import Optional, Union

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

Edge cases NOT handled (skip via precondition)

  • Pydantic v1 models or get_type_hints in the same file without __future__.annotationsruntime_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 NameErrors.
  • from typing import * — every target name is conservatively assumed in scope but no cleanup is performed.
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+.