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. The lint is non-rewriting, so the diagnostic surfaces without touching the source.

Configuration

KeyTypeDefaultMeaning
enabledbooltrueToggle 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.

The Canonical Case

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

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

More Examples

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 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.

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.