ruff RUF070
labelled unknown
Analysis Nemotron 3 Super
Flags redundant variable assignments directly before yielding the same variable.
Fix: Replace 'x = expr\n yield x' with 'yield expr', preserving indentation and removing the assignment line while keeping all other code unchanged.
12 hazards
- Removing the assignment eliminates the local variable binding, altering the output of locals() inspection during generator suspension.
- If the variable is referenced in a debugger or introspection tool at the yield point, the reference will become undefined after the fix.
- The assignment may contain a side-effect (e.g., incrementing a counter via a property setter) that would be omitted by the fix.
- If the assigned expression is used elsewhere in the function via closure capture, removing the assignment could break the closure by eliminating the variable reference.
- The assignment line may carry a trailing comment that would be deleted by the rewrite, altering source documentation.
- If the variable name is shadowed in an inner scope, removing the outer assignment could change which variable is yielded due to altered resolution.
- The assignment may be part of a larger statement (e.g., walrus operator) that the rule does not handle, but if misapplied could break syntax.
- If the expression is a mutable object and the variable is later mutated (though not yielded), the fix does not affect this, but the hazard is misdiagnosis—however, the rule only fires when unused, so this is mitigated; still, if the variable is used in a lambda defined later but not executed, the lambda's closure could break.
- In rare cases, the assignment might be syntactically indistinguishable from a type annotation in stub files or conditional code, leading to incorrect removal.
- If the yield is inside a try block and the assignment contains a finally-dependent expression, moving the expression into the yield could change exception timing.
- The fix may interfere with instrumented code (e.g., coverage tools) that relies on the assignment line being executable.
- If the assignment uses the walrus operator in a way that creates a variable used only in the yield, the rule may not fire due to current limitations, but if incorrectly extended, it could break.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Mutable object mutation after assignment | If the expression is a mutable object and the variable is later mutated (though not yielded), the fix does not affect this, but the hazard is misdiagnosis—however, the rule only fires when unused, so |
| r1-p002 | fired, no fix | Property descriptor with side effects | class attributes vs instance attributes, descriptors, properties with side effects |
| r1-p003 | silent prediction off | Closure capture in nested lambda | If the assigned expression is used elsewhere in the function via closure capture, removing the assignment could break the closure by eliminating the variable reference. |
| r1-p004 | silent prediction off | Variable shadowing in inner scope | If the variable name is shadowed in an inner scope, removing the outer assignment could change which variable is yielded due to altered resolution. |
| r1-p005 | fired, no fix prediction off | Star-import and __all__ interaction | star-imports, module-level side effects, __all__ |
| r1-p006 | silent prediction off | Numeric coercion and precision | int/float/bool coercion, Decimal, division and rounding |
| r1-p007 | silent prediction off | Locals() inspection during generator suspension | Removing the assignment eliminates the local variable binding, altering the output of locals() inspection during generator suspension. |
| r1-p008 | fired, no fix prediction off | Walrus operator in assignment (near-miss) | If the assignment uses the walrus operator in a way that creates a variable used only in the yield, the rule may not fire due to current limitations, but if incorrectly extended, it could break. |
| r2-p001 | silent prediction off | Type annotation confusion | In rare cases, the assignment might be syntactically indistinguishable from a type annotation in stub files or conditional code, leading to incorrect removal. |
| r2-p002 | silent prediction off | Evaluation order and side effects | Evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p003 | silent | Walrus operator interaction | The assignment may be part of a larger statement (e.g., walrus operator) that the rule does not handle, but if misapplied could break syntax. |
| r2-p004 | fired, no fix | Runtime type annotations | Type annotations evaluated at runtime, typing-only imports |
| r2-p005 | fired, no fix | Class vs instance attributes | Class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p006 | fired, no fix | Exception handling interference | Exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p007 | silent prediction off | Locals() inspection during suspension | Removing the assignment eliminates the local variable binding, altering the output of locals() inspection during generator suspension. |
| r2-p008 | silent | Misapplied walrus extension | If the assignment uses the walrus operator in a way that creates a variable used only in the yield, the rule may not fire due to current limitations, but if incorrectly extended, it could break. |
Sources
r1-p001 · Mutable object mutation after assignment
1def gen():
2 x = []
3 yield x
4 # Lambda captures x but not executed until after yield
5 f = lambda: len(x)
6 x.append(1) # Mutation after yield
7 yield f() # Should reflect mutation
8
9print(list(gen()))
[[1], 1]
r1-p002 · Property descriptor with side effects
1class A:
2 @property
3 def prop(self):
4 print('prop accessed')
5 return 42
6
7def gen():
8 a = A()
9 x = a.prop # Triggers property access
10 yield x
11 return
12
13list(gen()) # Should print 'prop accessed' once
prop accessed
r1-p003 · Closure capture in nested lambda
1def gen():
2 x = 10
3 yield x
4 # Lambda captures x, called after first yield
5 f = lambda: x * 2
6 yield f() # Should be 20
7
8print(list(gen()))
[10, 20]
r1-p004 · Variable shadowing in inner scope
1def gen():
2 x = 99
3 def inner():
4 x = 88 # Shadows outer x
5 return x
6 yield x # Should yield 99 (outer)
7 yield inner() # Should yield 88
8
9print(list(gen()))
[99, 88]
r1-p005 · Star-import and __all__ interaction
1# This file tests star-import safety
2# We create a temporary module to isolate
3import tempfile
4import os
5
6code = '''
7__all__ = ['value']
8value = 123
9'''
10
11with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
12 f.write(code)
13 modname = f.name[:-3]
14
15try:
16 # Import the module
17 import importlib.util
18 spec = importlib.util.spec_from_file_location(modname, f.name)
19 module = importlib.util.module_from_spec(spec)
20 spec.loader.exec_module(module)
21
22 def gen():
23 x = module.value # Assignment from imported module
24 yield x
25 # No further use
26
27 print(list(gen()))
28finally:
29 os.unlink(f.name)
[123]
r1-p006 · Numeric coercion and precision
1from decimal import Decimal, getcontext
2getcontext().prec = 10
3
4def gen():
5 x = Decimal('1') / Decimal('3') # Precision-sensitive
6 yield x
7 # Later use in calculation (not yielded)
8 y = x * 3
9 # y should be 1 if precision preserved
10 yield (y - 1).copy_abs() # Should be near zero
11
12print(list(gen()))
[Decimal('0.3333333333'), Decimal('1E-10')]r1-p007 · Locals() inspection during generator suspension
1def gen():
2 x = 42
3 frame = None
4 def capture_frame():
5 nonlocal frame
6 frame = frame # This is a trick: we want to capture the frame at yield point
7 # Instead, we inspect locals at first yield
8 pass
9 # We'll inspect locals by yielding a frame proxy
10 yield x
11 # Now yield locals() to see if 'x' is present
12 # But we can't access frame directly -> alternative: use sys._getframe
13 import sys
14 yield sys._getframe().f_locals.copy()
15
16# First value is 42, second is locals dict
17g = gen()
18first = next(g)
19second = next(g)
20print(f'First yielded: {first}')
21print(f"'x' in locals: {'x' in second}")
22print(f"Value of 'x' in locals: {second.get('x', 'MISSING')}")
First yielded: 42 'x' in locals: True Value of 'x' in locals: 42
r1-p008 · Walrus operator in assignment (near-miss)
1def gen():
2 if (x := 5) > 0: # Walrus in condition
3 yield x # Uses x from walrus
4 # Rule should NOT fire here because assignment is not directly before yield
5 # (there's an if statement in between)
6
7 # Second part: direct assignment that SHOULD trigger
8 y = 99
9 yield y
10
11print(list(gen()))
[5, 99]
r2-p001 · Type annotation confusion
1def gen():
2 x: int = 42
3 yield x
4print(list(gen()))
[42]
r2-p002 · Evaluation order and side effects
1seq = []
2def side():
3 seq.append('called')
4 return 99
5def gen():
6 x = side()
7 yield x + sum(seq)
8print(list(gen()))
r2-p003 · Walrus operator interaction
1def gen():
2 if (x := 5) > 0:
3 yield x
4print(list(gen()))
[5]
r2-p004 · Runtime type annotations
1T = int
2def gen():
3 x = T(7)
4 yield x
5print(list(gen()))
[7]
r2-p005 · Class vs instance attributes
1class A:
2 val = 100
3 @property
4 def prop(self):
5 return self.val * 2
6def gen():
7 a = A()
8 x = a.prop
9 yield x
10print(list(gen()))
[200]
r2-p006 · Exception handling interference
1def gen():
2 try:
3 x = 1 / 0
4 except ZeroDivisionError as e:
5 x = e
6 yield x
7print(list(gen()))
[ZeroDivisionError('division by zero')]r2-p007 · Locals() inspection during suspension
1def gen():
2 x = 42
3 yield locals().get('x', 'missing')
4print(list(gen()))
[42]
r2-p008 · Misapplied walrus extension
1def gen():
2 while (x := next(it)) != 0:
3 yield x
4it = iter([3, 2, 1, 0])
5print(list(gen()))
[3, 2, 1]