ruff FURB187
labelled unknown
Analysis Nemotron 3 Super
Flags inefficient list reversal patterns like assignment via slicing, reversed(), or list(reversed()) when .reverse() could be used.
Fix: Replaces the assignment target variable with an in-place .reverse() call on the same variable, e.g., 'l = l[::-1]' becomes 'l.reverse()'.
12 hazards
- If the list is aliased elsewhere (e.g., another variable references the same list), mutating via .reverse() affects all aliases, whereas slicing creates a new list leaving original aliases unchanged.
- If the reversed result is used immediately in an expression (e.g., function call), replacing with .reverse() breaks because .reverse() returns None, altering return values or side effects.
- If the assignment is part of a larger expression (e.g., tuple unpacking or conditional), rewriting to a statement changes syntax and may cause invalid Python.
- If the variable is deleted or reassigned later, the timing of mutation vs. creation affects memory usage and reference counting, potentially impacting __del__ methods or weakrefs.
- If the list contains mutable elements and the code relies on the original list being unchanged for later use, .reverse() causes premature mutation.
- If the slice operation includes step or bounds (e.g., l[::-1] vs l[1:5:-1]), the fix incorrectly applies .reverse() which only does full reversal, changing semantics.
- If the variable is a property or item assignment (e.g., self.lst = self.lst[::-1]), the fix cannot be applied as .reverse() requires a direct variable name, not an attribute or subscript.
- If the code depends on the identity of the list object (e.g., checking 'l is original' after reversal), .reverse() preserves identity while slicing creates a new object, breaking identity checks.
- If the reversed list is iterated over before reassignment (e.g., for i in l[::-1]: ...; l = l[::-1]), moving .reverse() earlier changes iteration order due to premature mutation.
- If the list reversal is guarded by a condition (e.g., if flag: l = l[::-1]), the fix must stay within the block; extracting .reverse() out of scope breaks conditioning.
- If the variable is used in a lambda or comprehension after reversal but before reassignment, early mutation via .reverse() alters the iterable seen by the lambda/comprehension.
- If the assignment occurs in a class or module level and the list is shared across instances or imports, in-place mutation introduces cross-instance state sharing bugs.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Mutable element reliance | If the list contains mutable elements and the code relies on the original list being unchanged for later use, .reverse() causes premature mutation. |
| r1-p002 | silent prediction off | Immediate use in expression | If the reversed result is used immediately in an expression (e.g., function call), replacing with .reverse() breaks because .reverse() returns None, altering return values or side effects. |
| r1-p003 | silent | Larger expression context | If the assignment is part of a larger expression (e.g., tuple unpacking or conditional), rewriting to a statement changes syntax and may cause invalid Python. |
| r1-p004 | silent prediction off | File modes and truncation | file modes and truncation, partial writes, non-str data passed to write |
| r1-p005 | silent prediction off | Property or item assignment | If the variable is a property or item assignment (e.g., self.lst = self.lst[::-1]), the fix cannot be applied as .reverse() requires a direct variable name, not an attribute or subscript. |
| r1-p006 | silent prediction off | Mutable default arguments and aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p007 | silent prediction off | Class vs instance attributes | class attributes vs instance attributes, descriptors, properties with side effects |
| r1-p008 | silent prediction off | Pathlib vs os.path behavior | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r2-p001 | silent prediction off | Async generator yields different results due to delayed mutation | async def/await ordering, async generators |
| r2-p002 | fired, no fix | Deletion after reversal affects weakref callback timing | If the variable is deleted or reassigned later, the timing of mutation vs. creation affects memory usage and reference counting, potentially impacting __del__ methods or weakrefs. |
| r2-p003 | fired, no fix | Conditional reversal inside if-block must not be lifted | If the list reversal is guarded by a condition (e.g., if flag: l = l[::-1]), the fix must stay within the block; extracting .reverse() out of scope breaks conditioning. |
| r2-p004 | silent prediction off | Keyword-only argument captures original list before mutation | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p005 | fired, no fix | Comment attached to reversal line is lost after fix | comments and docstrings attached to the removed or moved code |
| r2-p006 | silent prediction off | Identity check fails after fix because new object created | If the code depends on the identity of the list object (e.g., checking 'l is original' after reversal), .reverse() preserves identity while slicing creates a new object, breaking identity checks. |
| r2-p007 | silent prediction off | Bytes reversal produces mojibake if decoded as text later | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p008 | fired, no fix | Float list reversal affects later sum due to precision | int/float/bool coercion, Decimal, division and rounding |
Sources
r1-p001 · Mutable element reliance
1outer = [[1], [2]]
2inner = outer
3lst = inner[::-1]
4print('Before:', [x[0] for x in outer])
5lst[0].append(99)
6print('After modification through lst:', [x[0] for x in outer])
Before: [1, 2] After modification through lst: [1, 2]
r1-p002 · Immediate use in expression
1def get_first_rev(x):
2 return list(reversed(x))[0]
3try:
4 print(get_first_rev([1, 2, 3]))
5except Exception as e:
6 print(type(e).__name__, e)
3
r1-p003 · Larger expression context
1data = [1, 2, 3]
2if (tmp := data[::-1]) and tmp[0] == 3:
3 print('matched', tmp)
4else:
5 print('no match')
matched [3, 2, 1]
r1-p004 · File modes and truncation
1with open('test.bin', 'wb') as f:
2 data = [b'\x00', b'\x01']
3 f.write(bytes().join(data[::-1]))
4with open('test.bin', 'rb') as f:
5 print(f.read())
6import os
7os.remove('test.bin')
b'\x01\x00'
r1-p005 · Property or item assignment
1class Container:
2 def __init__(self):
3 self._lst = [1, 2, 3]
4 @property
5 def lst(self):
6 return self._lst
7 @lst.setter
8 def lst(self, v):
9 self._lst = v
10c = Container()
11c.lst = c.lst[::-1]
12print(c._lst)
[3, 2, 1]
r1-p006 · Mutable default arguments and aliasing
1def f(x=[]):
2 x = x[::-1]
3 return x
4print(f([1, 2]))
5print(f())
6print(f())
[2, 1] [] []
r1-p007 · Class vs instance attributes
1class A:
2 lst = [1, 2, 3]
3 def meth(self):
4 self.lst = self.lst[::-1]
5a = A()
6a.meth()
7print('class:', A.lst)
8print('instance:', a.lst)
class: [1, 2, 3] instance: [3, 2, 1]
r1-p008 · Pathlib vs os.path behavior
1from pathlib import Path
2p = Path('a')
3parts = list(p.parts)[::-1]
4print('reversed parts:', parts)
5try:
6 Path(*parts).resolve()
7except Exception as e:
8 print(type(e).__name__, str(e)[:50])
reversed parts: ['a']
r2-p001 · Async generator yields different results due to delayed mutation
1async def gen(l):
2 yield l
3 l = l[::-1]
4 yield l
5
6async def main():
7 l = [1, 2, 3]
8 g = gen(l)
9 print('first:', await g.__anext__())
10 print('second:', await g.__anext__())
11
12import asyncio
13asyncio.run(main())
first: [1, 2, 3] second: [3, 2, 1]
r2-p002 · Deletion after reversal affects weakref callback timing
1import weakseq
2callbacks = []
3def callback(ref):
4 callbacks.append('called')
5l = [1, 2, 3]
6r = weakref.ref(l, callback)
7l = l[::-1]
8del l
9print('callbacks:', callbacks)
10print('r() is None:', r() is None)
r2-p003 · Conditional reversal inside if-block must not be lifted
1flag = True
2l = [1, 2, 3]
3if flag:
4 l = l[::-1]
5 print('inside if:', l)
6print('after if:', l)
inside if: [3, 2, 1] after if: [3, 2, 1]
r2-p004 · Keyword-only argument captures original list before mutation
1def f(*, lst=[1, 2, 3]):
2 lst = lst[::-1]
3 return lst
4print('first call:', f())
5print('second call:', f())
first call: [3, 2, 1] second call: [3, 2, 1]
r2-p005 · Comment attached to reversal line is lost after fix
1# This reversal is critical for state
2l = [1, 2, 3]
3l = l[::-1] # Must happen before logging
4print('reversed:', l)
reversed: [3, 2, 1]
r2-p006 · Identity check fails after fix because new object created
1orig = [1, 2, 3]
2l = orig
3l = l[::-1]
4print('l is orig:', l is orig)
5print('l:', l)
6print('orig:', orig)
l is orig: False l: [3, 2, 1] orig: [1, 2, 3]
r2-p007 · Bytes reversal produces mojibake if decoded as text later
1b = b'hello\nworld'
2b = b[::-1]
3s = b.decode('utf-8')
4print('reversed bytes:', b)
5print('decoded string:', repr(s))
reversed bytes: b'dlrow\nolleh' decoded string: 'dlrow\nolleh'
r2-p008 · Float list reversal affects later sum due to precision
1l = [0.1, 0.2, 0.3]
2s1 = sum(l)
3l = l[::-1]
4s2 = sum(l)
5print('sum before:', s1)
6print('sum after:', s2)
7print('equal?', s1 == s2)
sum before: 0.6 sum after: 0.6 equal? True