Skip to content

single-use-variables

A binding that's assigned once and read once usually exists because the author wanted a name for the expression, and the name reads better than the expression at the call site. Sometimes that's a real win, and sometimes the binding is just standing in for inlining the right-hand side.

surfaces bindings assigned and read exactly once, leaving the inline-or-keep decision to a future refactor pass that picks up the lint output.

The rule consumes the per-Source BindingAnalysis table to count writes and reads per binding. Bindings matching the allow-pattern regex (defaulting to ^_, which exempts intentionally-unused names) stay quiet. Augmented assignments count as both a write and a read, so a binding they target isn't single-use. Loop variables, comprehension targets, and function parameters are introduced implicitly and stay outside the rule's reach. A tuple-unpack target stays exempt when a sibling target reads more than once, because removing one element would split the unpack into an indexed read. When every target reads once, the diagnostic names the subscript rewrite (batch[0] for the first target of x, y = batch) whenever the right-hand side is a plain name or attribute. A walrus bound in the test of an if, elif, or while is exempt because the test already consumes the value, so the single later read is the second use, and inlining would recompute the guarded expression in both the test and the body. The lint is non-rewriting, so the diagnostic surfaces without touching the source.

Configuration

KeyTypeDefaultMeaning
enabledbooltrueToggles the rule on or off.
allow-patternregex"^_"Binding names exempted from the lint.

The default ^_ exempts names starting with an underscore, matching the Python convention for intentionally-unused bindings. Projects with stricter naming can tighten the regex, and an empty pattern reads as exempting nothing rather than everything, the same reading

gives its own empty default.

The Canonical Case

A binding assigned and read exactly once surfaces the lint, recommending inlining the right-hand side.

def basic(arg):
    x = expensive(arg)
    return x + 1
python

More Examples

A subscript rewrite is named only when the right-hand side is a plain name or attribute. Subscripting a call as lookup()[0] and lookup()[1] would evaluate it twice, so the lint flags each target without naming a rewrite.

A binding captured by a nested closure is flagged because the analyzer attributes the closure's read to the outer scope, leaving it single-use. Inlining shifts evaluation timing when the right-hand side carries side effects, so the diagnostic invites review rather than a blind rewrite.

A subscript rewrite is named when the right-hand side is a plain name or an attribute access. With x, y = point.coords the lint flags each target and names the rewrite off the attribute (point.coords[0] for the first, point.coords[1] for the second).

A single-use binding inside an async def is flagged the same way as one inside a plain def. The rule consumes the unified function-def shape and treats both as the same scope kind, so await-driven code earns no exemption.

When every target of first, second = batch reads exactly once, the whole statement can dissolve into subscripts, so the lint flags each target and names the rewrite (batch[0] for the first, batch[1] for the second).

No Change

A # fmt: off block drops the diagnostic for any single-use binding inside the suppressed span. The pipeline filters Severity::Lint diagnostics by range alongside edits, so suppression covers lint output and rewrites alike.

No Change

A user-supplied allow-pattern replaces the default ^_, so names matching a project's local convention pass through unflagged. Here the tmp_ prefix is the configured exemption, sparing tmp_value from the diagnostic.

No Change

An augmented assignment such as total += value is both a read and a write of its target, so the binding's write count climbs above one and the rule leaves it alone. Inlining a name that mutates itself is never the intended cleanup.

No Change

A comprehension target such as x in [x * x for x in xs] lives in the comprehension's own scope, not the enclosing function, so its lone use never enters the function-scope enumeration the rule walks.

No Change

A function whose body declares global is skipped entirely, because an accurate single-use reading would have to follow the binding out through the module chain. The whole scope is exempted rather than guessed at.

For per-line opt-outs, the Suppression chapter covers the # prose: ignore[single-use-variables] directive.