unused-future-annotations
FormattingRemoves from __future__ import annotations lines that no longer carry their weight on the target Python version.
Optional[T], Union[X, Y], and List[int] all come from the typing module and were the canonical spellings for years. The language has since absorbed each of them, with PEP 585 giving the generics their builtin form on Python 3.9 and PEP 604 giving the unions their | form on Python 3.10. Both modern spellings read more directly than the ones they replace, and together they retire the typing import a module carried only to reach them.
Each rewrite runs behind its own facet and its own version floor, so a project on 3.9 converts its generics while its unions wait for 3.10. A project that has set no target-version at all holds both, since an unset field clears neither floor.
rewrite-generics converts the typing generics whose PEP 585 replacement is a builtin, covering Dict, FrozenSet, List, Set, Tuple, and Type. A generic whose replacement lives under collections instead (Deque, DefaultDict) stays as written, because reaching it would mean adding an import this rewrite does not add.
rewrite-unions joins the members of an Optional or a Union with |, appending the | None arm an Optional implies. A member that cannot carry the operator holds the whole annotation back, which covers a forward-reference string such as Optional["Node"], where the rewritten "Node" | None would raise at evaluation time.
Both facets resolve their target through whatever name the module bound, so a bare Optional, a module-qualified typing.Optional, an aliased Optional as Opt, and the typing_extensions spelling of any of them all reach the same rewrite.
A typing name the rewrite read out entirely leaves its import binding unread, and the rule drops it in the same pass. The accounting runs per name, so an import naming several members keeps the ones something still reads:
cast is read at runtime and survives, so the alias beside it goes on its own rather than the whole import line.
from typing import cast
def widen(value: int | None) -> object:
return cast(object, value)
A read the rewrite could not consume counts the same as any other, so a suppressed line or a held forward reference keeps the import standing. The Suppression chapter covers the directives that hold a line.
Both facets fire, so an annotation carrying both legacy spellings settles in one pass.
Below 3.10 only rewrite-generics fires, since X | Y raises at runtime before the PEP 604 form lands, and below 3.9 neither one does.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | true | Toggles the rule on or off. |
rewrite-generics | bool | true | Converts a typing generic to the builtin PEP 585 gave it, so List[int] reads as list[int]. Runs on target-version 3.9 and higher, and false leaves every typing generic in place. |
rewrite-unions | bool | true | Rewrites Optional[X] and Union[X, Y] to the PEP 604 X | None and X | Y forms. Runs on target-version 3.10 and higher, and false leaves every legacy union in place. |
The target-version field from the top-level Configuration gates each facet per project, and an unset field holds both.
An annotation carrying both legacy spellings settles in one pass, with typing.List[int] reaching its builtin and typing.Optional[...] reaching the PEP 604 pipe. The typing import goes with them, since nothing reads it once the rewrites land.
x: list[int] | None = None
PEP 585 gave each of these a builtin spelling on Python 3.9, so the whole typing import goes once the last one converts.
cast is read at runtime and survives, so the alias beside it goes on its own rather than the whole import line.
Optional[int] reads as int | None from Python 3.10 on, and the import that bound Optional drops with it.
The union facet runs while the generic facet is off, so List keeps both its spelling and its import.
The generic facet runs while the union facet is off, so Optional keeps both its spelling and its import.
X | None is runtime-valid from 3.10, so a 3.9 target converts the generic and leaves the union alone. The import keeps the one name still read.
Each member of a Union joins with the PEP 604 |, in their order inside the subscript.
The rewritten "Node" | None raises at evaluation time, in that a str carries no |, so the whole annotation stays legacy.
Deque maps to collections.deque rather than to a builtin, and the rewrite adds no import, so the annotation stays as written.
With no target declared, an unset field clears neither version floor, so the annotation passes through untouched.
Removes from __future__ import annotations lines that no longer carry their weight on the target Python version.
For the gate semantics, target-version in the Configuration chapter covers how the field is read across version-gated rules.