fixhunt

ruff PLR6104

labelled unknown
Runs / 2026-10-17T21-06-20-465Z-zjiy-ruff-PLR6104ruff 0.17.12026-10-17 21:06 UTC8 programs × 2 round(s)16 programs, 9 fired, 0 fixed, 0 findings, 3 rejectedmodel cost $0.01

Analysis Nemotron 3 Super

Flags simple assignments where the target variable appears on the left-hand side of a binary operation on the right-hand side, suggesting replacement with an augmented assignment.

Fix: Replaces the assignment statement with an augmented assignment using the same operator (e.g., 'x = x + 1' becomes 'x += 1'), preserving the left-hand side target and the right-hand side operand excluding the target.

10 hazards
  • For mutable targets like lists, the rewrite changes semantics by mutating the original object in-place, affecting all aliases (e.g., 'foo = foo + [2]' vs 'foo += [2]' when other variables reference the same list).
  • For immutable targets like strings, the rewrite may be semantically invalid if the target is not the left operand (e.g., 'x = "prefix-" + x' cannot be safely rewritten as 'x += "prefix-"' due to non-commutativity).
  • If the right-hand side contains side effects (e.g., function calls), the rewrite may alter evaluation order or frequency (though PLR6104 typically avoids such cases, complex expressions might still pose risks if misidentified).
  • For numeric types with user-defined __add__ or __iadd__ methods, the rewrite may invoke different methods, leading to divergent behavior if the methods are not equivalent.
  • In NumPy arrays, augmented assignment may fail due to shape or dtype constraints where the plain assignment succeeds via broadcasting or promotion (e.g., in-place multiplication requiring matching shapes).
  • The rewrite removes any inline comments present in the original statement, as the entire line is replaced.
  • If the left-hand side is a complex target (e.g., attribute or subscript), the rule may not fire, but if it did incorrectly, the rewrite could misinterpret scoping or evaluation (e.g., 'self.x = self.x + 1' is safe, but misfiring on non-simple targets could break).
  • For targets that are not simple names (e.g., tuples or destructuring), the rule does not apply, but if erroneously triggered, the rewrite would be syntactically invalid.
  • If the assignment is part of a larger statement (e.g., in a loop or conditional), the rewrite preserves structure but may affect debugging or profiling tools that rely on exact line numbers or bytecode.
  • In rare cases involving descriptors or properties with __set__, the augmented assignment may trigger __iadd__ directly, bypassing __set__ and altering expected behavior.

Programs

idresulttitlehazard
r1-p001fired, no fixbytes/str mode interactionAugmented assignment changes evaluation order when rhs involves decoding bytes; original evaluates lhs first then rhs, whereas += may evaluate differently if __iadd__ mutates state.
r1-p002fired, no fixmutable default argument aliasingRewriting x = x + [y] to x += [y] shares the list via default argument, causing cross-call mutation; original creates new list each time.
r1-p003fired, no fixnon-file context manager with write()Augmented assignment on buffer-like object may call __iadd__ which mutates in-place, breaking context manager assumptions if write() buffers internally.
r1-p004silent prediction offloop line number sensitivityIn a loop, rewritten assignment changes bytecode line numbers; debuggers/tracers relying on original line numbers may misattribute state.
r1-p005fired, no fixkeyword-only and defaultsDefault argument evaluation timing interacts with augmented assignment if default is mutable and rewritten assignment aliases it.
r1-p006fired, no fixf-string and repr side effectsIf rhs contains f-string with side-effect expression, original evaluates lhs then rhs; += may change evaluation order if __iadd__ called mid-format.
r1-p007fired, no fixdescriptor and property interactionRewriting attribute assignment to augmented assignment bypasses __set__ if __iadd__ exists, calling descriptor's __get__ then __iadd__ then __set__, altering side effects.
r1-p008invalidpathlib vs os.path symlink resolutionAugmented assignment on Path object may resolve symlinks differently if __iadd__ implements path concatenation with side effects (e.g., auto-normalize).
r2-p001invalidUser-defined __add__ vs __iadd__ divergenceFor numeric types with user-defined __add__ or __iadd__ methods, the rewrite may invoke different methods, leading to divergent behavior if the methods are not equivalent.
r2-p002silent prediction offShadowed builtins and names redefined latershadowed builtins and names redefined later in the module
r2-p003silentImmutable strings non-commutativityFor immutable targets like strings, the rewrite may be semantically invalid if the target is not the left operand (e.g., 'x = "prefix-" + x' cannot be safely rewritten as 'x += "prefix-"' due to non-c
r2-p004fired, no fixDescriptor __set__ bypassed by __iadd__In rare cases involving descriptors or properties with __set__, the augmented assignment may trigger __iadd__ directly, bypassing __set__ and altering expected behavior.
r2-p005fired, no fixContext manager write() without close()context managers that are not files, objects with write() but no close()
r2-p006fired, no fixNumPy array shape/dtype constraint failureIn NumPy arrays, augmented assignment may fail due to shape or dtype constraints where the plain assignment succeeds via broadcasting or promotion (e.g., in-place multiplication requiring matching sha
r2-p007invalidMutable list aliasing mutationFor mutable targets like lists, the rewrite changes semantics by mutating the original object in-place, affecting all aliases (e.g., 'foo = foo + [2]' vs 'foo += [2]' when other variables reference th
r2-p008silent prediction offAsync def/await ordering interferenceasync def/await ordering, async generators

Sources

r1-p001 · bytes/str mode interaction
1buf = []
2class B:
3 def __init__(self, v): self.v = v
4 def __radd__(self, other): buf.append('radd'); return self.v.decode() + other
5 def __iadd__(self, other): buf.append('iadd'); self.v += other.encode(); return self
6x = B(b'a')
7x = x + b'b'
8print(''.join(buf), x.v)
9# Trigger: x = x + b'b' -> x += b'b'
r1-p002 · mutable default argument aliasing
1def f(lst=None):
2 if lst is None: lst = []
3 lst = lst + [1] # Trigger
4 return lst
5print(f())
6print(f())
7# After fix: lst += [1] makes default [] mutable across calls
[1]
[1]
r1-p003 · non-file context manager with write()
1class Buf:
2 def __init__(self): self.data = []
3 def write(self, s): self.data.append(s)
4 def __iadd__(self, other): self.write(other); return self
5 def __enter__(self): return self
6 def __exit__(self, *a): pass
7with Buf() as b:
8 b = b + 'x' # Trigger
9 print(len(b.data))
10# After: b += 'x' still calls write once, but if __iadd__ were chained...
r1-p004 · loop line number sensitivity
1for i in range(2):
2 x = x + i if 'x' in locals() else 0 + i # Trigger on second iter
3 print(f'line={i}, x={x}')
4# After fix: line attribution shifts; if debugger breaks on 'x = x + i', it may not trigger
line=0, x=0
line=1, x=1
r1-p005 · keyword-only and defaults
1def g(a, *, lst=[]):
2 lst = lst + [a] # Trigger
3 return lst
4print(g(1))
5print(g(2))
6# After fix: lst += [a] shares default list -> [1, 2]
[1]
[2]
r1-p006 · f-string and repr side effects
1log = []
2class S:
3 def __init__(self, v): self.v = v
4 def __iadd__(self, other): log.append('iadd'); return S(self.v + str(other))
5 def __radd__(self, other): log.append('radd'); return str(other) + self.v
6x = S('a')
7x = x + f'{len(log)}' # Trigger
8print(''.join(log), x.v)
9# After: evaluation order of f-string vs __iadd__ may differ
r1-p007 · descriptor and property interaction
1class Desc:
2 def __get__(self, obj, owner): return obj._val if hasattr(obj, '_val') else 0
3 def __set__(self, obj, val): obj._val = val * 2
4class Holder:
5 x = Desc()
6h = Holder()
7h.x = h.x + 1 # Trigger: calls __get__ then __set__
8print(h._val)
9# After: h.x += 1 calls __get__, then __iadd__ (if defined), then __set__; but int has no __iadd__ so falls back -> same?
10# Make Desc have __iadd__? No, int doesn't. Instead:
11# Use mutable: h.x = h.x + [1]
12# Redo:
13h = Holder()
14h.x = h.x + [1] # Trigger
15print(h._val)
16# After fix: h.x += [1] -> if Desc had __iadd__? It doesn't. So still calls __get__ then __set__ with [1]+[1]? No.
17# Correct hazard: if property with setter, x = x + v calls getter then setter; x += v calls getter, then in-place if available, then setter.
18# For immutable, same. For mutable descriptor that returns list:
2
r1-p008 · pathlib vs os.path symlink resolution (generated program is not deterministic)
1import os
2os.symlink('tmp', 'link')
3with open('tmp', 'w') as f: f.write('hi')
4from pathlib import Path
5p = Path('link')
6p = p + '/../tmp' # Trigger: Path doesn't support + with str -> TypeError
7# Fix: use / operator
8p = Path('link')
9p = p / '../tmp' # Trigger
10print(p.exists())
11# After fix: p /= '../tmp' -> same
12# But if we override __truediv__? No.
13# Instead: make wrapper that logs
14class P:
15 def __init__(self, s): self.s = s
16 def __truediv__(self, other): return P(self.s + '/' + other)
17 def __itruediv__(self, other): self.s = self.s + '/' + other; return self
18 def exists(self): return os.path.exists(self.s)
19p = P('link')
20p = p / '../tmp' # Trigger
21print(p.exists())
22# After: p /= '../tmp' -> mutates self.s in place
r2-p001 · User-defined __add__ vs __iadd__ divergence (generated program is not deterministic)
1class A:
2 def __init__(self, v):
3 self.v = v
4 def __add__(self, other):
5 return A(self.v + other.v)
6 def __iadd__(self, other):
7 self.v *= 2
8 return self
9
10a = A(1)
11b = a
12a = a + A(1)
13print('plain:', a.v, id(a))
14print('alias:', b.v, id(b))
r2-p002 · Shadowed builtins and names redefined later
1sum = lambda x, y: x + y + 10
2x = 1
3x = sum(x, 2)
4print('after sum rebind:', x)
5sum = 99
6x = sum(x, 3) # Now uses built-in sum incorrectly
7print('after sum=int:', x)
after sum rebind: 13
r2-p003 · Immutable strings non-commutativity
1x = "hello"
2x = "prefix-" + x
3print('result:', x)
4# Rule does not fire: target x is not LHS of BinOp RHS
result: prefix-hello
r2-p004 · Descriptor __set__ bypassed by __iadd__
1class Descriptor:
2 def __get__(self, obj, objtype=None):
3 return obj._val if hasattr(obj, '_val') else 0
4 def __set__(self, obj, value):
5 obj._val = value * 2 # double on set
6
7class Owner:
8 attr = Descriptor()
9
10obj = Owner()
11obj.attr = 3 # triggers __set__ -> _val = 6
12print('after set:', obj.attr)
13obj.attr = obj.attr + 1 # plain assignment: __set__ with (6+1)=14
14print('after plain add:', obj.attr)
15obj.attr += 1 # augmented: triggers __iadd__ on descriptor? returns self? then __set__ may not be called
16print('after aug add:', obj.attr)
after set: 6
after plain add: 14
after aug add: 30
r2-p005 · Context manager write() without close()
1class Logger:
2 def __init__(self):
3 self.buf = []
4 def write(self, data):
5 self.buf.append(data)
6 def __enter__(self):
7 return self
8 def __exit__(self, *args):
9 pass
10
11log = Logger()
12with log as l:
13 msg = 'hello'
14 msg = msg + ' world'
15 l.write(msg)
16print('log buf:', log.buf)
log buf: ['hello world']
r2-p006 · NumPy array shape/dtype constraint failure
1import numpy as np
2arr = np.array([1, 2, 3], dtype=int)
3arr = arr * 2.5 # succeeds: promotes to float, new array
4print('plain result:', arr, arr.dtype)
5arr2 = np.array([1, 2, 3], dtype=int)
6try:
7 arr2 *= 2.5 # fails: in-place cannot change dtype
8 print('aug result:', arr2, arr2.dtype)
9except Exception as e:
10 print('aug failed:', type(e).__name__, str(e))
r2-p007 · Mutable list aliasing mutation (generated program is not deterministic)
1foo = [1]
2bar = foo # alias
3foo = foo + [2] # new list
4print('plain:', foo, bar, id(foo), id(bar))
5foo2 = [1]
6bar2 = foo2
7foo2 += [2] # mutates in-place
8print('aug:', foo2, bar2, id(foo2), id(bar2))
r2-p008 · Async def/await ordering interference
1async def inc(x):
2 await asyncio.sleep(0)
3 return x + 1
4
5async def main():
6 x = 1
7 x = await inc(x)
8 print('plain await:', x)
9 x += 1 # This is not awaitable; if x were a future, would differ
10 print('aug after:', x)
11
12import asyncio
13asyncio.run(main())
plain await: 2
aug after: 3