Skip to content

unused-future-annotations

The from __future__ import annotations directive made forward-reference annotations possible on Python versions where the runtime evaluated annotations eagerly. PEP 749 lands deferred annotation evaluation in Python 3.14 by default whenever the file's annotations are typing-only, and the import becomes redundant.

removes the import when removal is provably safe for the file.

Three branches actually fire the rewrite. The file may carry zero annotations (the directive is unused outright). The target-version may be 3.14 or higher (the runtime defers annotation evaluation, so the directive carries no runtime weight). Or every name appearing in every annotation may resolve to a module-scope binding before its first annotation use (forward references aren't needed, so the runtime evaluates annotations eagerly without raising). When none of those branches holds, the import stays in place.

The version-gated branch stays quiet. Removal fires only if the file has zero annotations or every annotation resolves to a module-scope binding before use.

Configuration

KeyTypeDefaultMeaning
enabledbooltrueToggle the rule on or off

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

The Canonical Case

A file whose annotations are typing-only loses the __future__ import when the target version allows safe removal.

python


class Node:
    pass


def visit(node: Node) -> Node:
    return node

More Examples

An annotated assignment nested in a for loop body still marks the file as carrying annotations and routes through full binding resolution. The Item reference resolves to the class above the loop, so trigger 3 fires and the directive is removed.

The annotations alias sits last in a multi-name from __future__ import division, annotations. The surgical deletion strips the preceding comma alongside the alias, leaving the earlier division entry's formatting intact.

A from __future__ import annotations as legacy still imports the annotations feature regardless of the local binding name. The rule recognizes the directive through the as form and deletes the line.

An async def carries its return annotation in the same function-def shape as a sync def, with is_async flipped. The annotation probe sees it, and with Result defined ahead of the annotation the directive is removed.

The annotations alias leads a multi-name from __future__ import annotations, division. Only the annotations entry is removed, taking its separator comma with it, leaving division as the sole import.

No Change

A # fmt: off block wrapping the directive keeps it in place. The pipeline drops any edit whose range overlaps a suppressed span, so suppression is handled centrally rather than re-implemented per rule.

No Change

A class-body x: int annotation names the int builtin, which the module-scope binding analysis does not record. Trigger 3 fails conservatively and the directive stays, even though Python would resolve int at runtime.

For the gate semantics, target-version in the Configuration chapter covers how the field is read across version-gated rules.