Skip to content

legacy-union-syntax

Union[X, Y] and Optional[X] come from the typing module, were the canonical union shapes for years, and still read clearly today. On Python 3.10 and later, the PEP 604 pipe-union shapes (X | Y, X | None) read more directly and consume one fewer import, which over the course of a large codebase adds up to genuinely clearer type signatures.

surfaces the legacy form as a lint, leaving the rewrite to a future migration pass that picks up the lint output.

The rule fires only on projects whose target-version is 3.10 or higher, where the pipe-union shapes are runtime-supported. Pre-3.10 projects and projects with target-version unset stay quiet, since recommending the pipe form on those projects would mislead. The lint is non-rewriting, so the diagnostic surfaces without touching the source.

The rule fires. Optional[X] reads as X | None in the diagnostic message.

Configuration

KeyTypeDefaultMeaning
enabledbooltrueToggle the rule on or off

The target-version field from the top-level Configuration gates the lint per project.

The Canonical Case

A from typing import Optional followed by Optional[X] surfaces the lint, recommending X | None.

python
from typing import Optional

x: Optional[int] = None

More Examples

from typing import Optional as Opt binds Opt to typing.Optional, so Opt[int] resolves through the alias and flags the same as the bare name.

A legacy Optional[int] nested as the value type of dict[str, Optional[int]] is flagged on the inner expression. The enclosing subscript, rather than the statement, is passed as the parent for the parenthesized range.

Parentheses wrapping a subscript, as in (Optional[int]), are folded into the diagnostic range, so the surfaced span matches the author's mental model of the whole annotation.

The rule emits no edits, so every pass leaves the source byte-for-byte identical. A mix of legacy Optional[int] and Union[int, str] bindings alongside a modern int | None form all pass through as written.

After import typing, the annotation typing.Optional[int] resolves through the attribute chain back to typing.Optional and flags identically to the bare Optional[int] form.

No Change

Modern PEP 604 annotations written with | are never flagged. The walker inspects only Subscript shapes, so the binary BitOr expression backing int | None slips past entirely.

No Change

A typing import naming something other than Optional or Union is not flagged. The walker resolves List[int] to typing.List and falls through the suffix match's catch-all.

For per-line opt-outs, the Suppression chapter covers the # prose: ignore[legacy-union-syntax] directive.