fixhunt

ruff UP011: fix changes program behavior

fix-changes-behaviorlabelled safeintrospection-only · lowrelated
Findings / 2026-10-17T21-15-05-200Z-bfsc-ruff-UP011 / r2-p005ruff 0.17.113 → 9 lines2026-10-17 21:15 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

In code that manually inspects decorator syntax via ast or inspect, the absence of parentheses may alter the perceived call structure and break introspection logic.

Program

before
1import functools, ast
2@functools.lru_cache()
3def cached_func(x):
4 return x * 2
5tree = ast.parse(open(__file__).read())
6for node in ast.walk(tree):
7 if isinstance(node, ast.FunctionDef) and node.name == 'cached_func':
8 for dec in node.decorator_list:
9 print('Decorator is call:', len(dec.args) == 0 and len(dec.keywords) == 0)
after
1import functools, ast
2@functools.lru_cache
3def cached_func(x):
4 return x * 2
5tree = ast.parse(open(__file__).read())
6for node in ast.walk(tree):
7 if isinstance(node, ast.FunctionDef) and node.name == 'cached_func':
8 for dec in node.decorator_list:
9 print('Decorator is call:', len(dec.args) == 0 and len(dec.keywords) == 0)

Output

stdout beforeexit 0
Decorator is call: True
stdout afterexit 1
(no stdout)
AttributeError: 'Attribute' object has no attribute 'args'
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory introspection-only · low

The program parses its own source code and inspects the AST node for the decorator, checking whether the decorator call has zero arguments. After the fix removes the parentheses, the AST node changes from a Call to an Attribute, so the introspection logic fails with an AttributeError. This is a change only in self-inspection of the rewritten source text, not in the runtime semantics of the decorated function.

Explanation Nemotron 3 Super unverified

The original decorator `@functools.lru_cache()` calls `lru_cache` with no arguments, returning a decorator function that wraps `cached_func`. After rewriting to `@functools.lru_cache`, the decorator becomes the raw function object itself, not its result; thus it does not wrap the function, leaving `cached_func` undecorated. During AST traversal, `dec` now refers to an `Attribute` node (`functools.lru_cache`) instead of a `Call` node, so accessing `.args` raises an AttributeError because attributes lack argument lists. This changes both behavior and output.

Reproduce

file
prog.py (the before program)
minimized program
r2-p005.py
evidence sheet
r2-p005.md
regression fixture
r2-p005.py the minimized program with a header stating the observed change, for the tool's test suite
python3 prog.py
ruff check --preview --isolated --select UP011 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

UP011 fix · UP011 decorator parentheses removal · lru_cache empty call transformation · AST FunctionDef decorator args access

  • related astral-sh/ruff/issues/1913 Same UP011 rule but different construct: that report is about `lru_cache(max_size=None)` → `lru_cache`, this one is about `lru_cache()` → `lru_cache`.
  • related astral-sh/ruff/issues/827 Meta-issue tracking pyupgrade implementation including UP011, not the specific `lru_cache()` → `lru_cache` defect.

Details

program
AST introspection of decorator call structure
hazard
In code that manually inspects decorator syntax via ast or inspect, the absence of parentheses may alter the perceived call structure and break introspection logic.
oracle
stdout before: Decorator is call: True stdout after: exit 0 → 1 stderr before: (none) stderr after: AttributeError: 'Attribute' object has no attribute 'args'
run
2026-10-17T21-15-05-200Z-bfsc-ruff-UP011
generated program (13 lines) and the fix's diff
1import functools, ast
2@functools.lru_cache()
3def cached_func(x):
4 return x * 2
5tree = ast.parse(open(__file__).read())
6for node in ast.walk(tree):
7 if isinstance(node, ast.FunctionDef) and node.name == 'cached_func':
8 for dec in node.decorator_list:
9 if isinstance(dec, ast.Call) and dec.func.attr == 'lru_cache':
10 print('Decorator is call:', len(dec.args) == 0 and len(dec.keywords) == 0)
11 elif isinstance(dec, ast.Attribute) and dec.attr == 'lru_cache':
12 print('Decorator is attribute only:', dec.attr)
13print('Result:', cached_func(5))
--- orig.py	2026-10-17 17:15:47.523352220 -0400
+++ fixed.py	2026-10-17 17:15:47.705339077 -0400
@@ -1,5 +1,5 @@
 import functools, ast
-@functools.lru_cache()
+@functools.lru_cache
 def cached_func(x):
     return x * 2
 tree = ast.parse(open(__file__).read())
rule documentation given to the models
# lru-cache-without-parameters (UP011)

Derived from the **pyupgrade** linter.

Fix is always available.

## What it does
Checks for unnecessary parentheses on `functools.lru_cache` decorators.

## Why is this bad?
Since Python 3.8, `functools.lru_cache` can be used as a decorator without
trailing parentheses, as long as no arguments are passed to it.

## Example

```python
import functools


@functools.lru_cache()
def foo(): ...
```

Use instead:

```python
import functools


@functools.lru_cache
def foo(): ...
```

## Options
- `target-version`

## References
- [Python documentation: `@functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)
- [Let lru_cache be used as a decorator with no arguments](https://github.com/python/cpython/issues/80953)