ruff FURB145
labelled unknown
Analysis Nemotron 3 Super
Flags unbounded slice expressions (`seq[:]`) used to copy a sequence when the target is known to be a list.
Fix: Replaces the unbounded slice expression `seq[:]` with the method call `seq.copy()` while preserving the surrounding assignment or expression context.
11 hazards
- Rewriting `seq[:]` where `seq` is not a list (e.g., a string or tuple) changes the return type from the original sequence type to a list, breaking type expectations.
- If `seq` is a subclass of list that overrides `__getitem__` but not `copy`, the slice may return a different type than `copy()` produces.
- When the slice appears in a context relying on the exact object identity of a new list (e.g., `is` checks), `copy()` may produce a different object than slicing in some implementations (though CPython guarantees a new object, this is not language-spec guaranteed).
- If `seq` is a variable that is deleted or reassigned between the evaluation of the slice and its use in the original code (impossible in a simple expression, but relevant in larger refactorings), the fix does not capture this timing—however, since the fix is applied locally to an expression, this hazard is mitigated; still noted for completeness in complex macro-like scenarios.
- If the slice expression contains an inline comment (e.g., `a[: # copy]`), the fix may drop or misplace the comment during rewrite, altering source code readability.
- In code where `seq` is a property with a side-effecting `__getitem__` (highly unconventional but possible), accessing `seq[:]` triggers `__getitem__` once with a slice object, whereas `seq.copy()` may not invoke `__getitem__` at all, changing side-effect timing.
- If `seq` is a mutable object and the slice is used in a conditional expression where the object might be mutated between evaluation points (not applicable here as the expression is atomic), but note that the fix does not alter evaluation order—it replaces the expression in place.
- When the slice is part of a larger subscript (e.g., `a[:][0]`), the fix only applies to the unbounded slice if isolated; however, the rule does not fire on bounded or chained slices, so this shape is silent—no hazard from misapplication.
- If `seq` is annotated as a list but inferred as a subtype (e.g., `UserList`), and `copy()` is not overridden, the returned object may lack expected subclass behavior, though `list.copy()` on a subclass returns a plain list, whereas slicing may preserve subclass type—this is a behavioral change.
- In code that depends on the slice returning exactly the same type as the input (e.g., for duck typing with custom sequences), replacing with `copy()` narrows the type to `list`, breaking polymorphism.
- If the slice appears in a lambda or comprehension where the variable is rebound later (e.g., `[f() for x in data][:]` vs `[f() for x in data.copy()]`), the fix does not change behavior because the slice is evaluated immediately—but note: the rule does not fire on `data[:]` inside a comprehension index; it fires on standalone unbounded slices.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Subclass override mismatch | If seq is a subclass of list that overrides __getitem__ but not copy, the slice may return a different type than copy() produces. |
| r1-p002 | silent prediction off | Polymorphism narrowing | In code that depends on the slice returning exactly the same type as the input (e.g., for duck typing with custom sequences), replacing with copy() narrows the type to list, breaking polymorphism. |
| r1-p003 | silent prediction off | Mutable default and aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p004 | silent prediction off | Parameter kinds and defaults | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p005 | silent prediction off | Dict ordering and overrides | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
| r1-p006 | fix ok | Async interaction | async def/await ordering, async generators |
| r1-p007 | silent | File write truncation | file modes and truncation, partial writes, non-str data passed to write |
| r1-p008 | silent prediction off | Exceptions and finally | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p001 | invalid | Conditional expression mutation timing | Conditional expression mutation timing - fix doesn't alter eval order but we verify sequencing by interleaving state reads/writes around the ternary op. |
| r2-p002 | fix ok prediction off | Identity check reliance | Object identity reliance: user expects distinct objects via [:], .copy() also gives distinct but future impl could differ; we detect identity via 'is'. |
Sources
r1-p001 · Subclass override mismatch
1class MyList(list):
2 def __getitem__(self, key):
3 return super().__getitem__(key) * 2
4lst = MyList([1, 2, 3])
5slice_copy = lst[:]
6method_copy = lst.copy()
7print('slice type:', type(slice_copy).__name__, 'values:', slice_copy)
8print('method type:', type(method_copy).__name__, 'values:', method_copy)
slice type: list values: [1, 2, 3, 1, 2, 3] method type: list values: [1, 2, 3]
r1-p002 · Polymorphism narrowing
1class Seq:
2 def __init__(self, data):
3 self.data = list(data)
4 def __getitem__(self, key):
5 return self.data[key]
6 def __len__(self):
7 return len(self.data)
8 def copy(self):
9 raise AttributeError('no copy')
10s = Seq([1, 2, 3])
11try:
12 sliced = s[:]
13except TypeError as e:
14 sliced = None
15 print('slice error:', type(e).__name__, str(e))
16try:
17 copied = s.copy()
18except AttributeError as e:
19 copied = None
20 print('copy error:', type(e).__name__, str(e))
21print('slice result:', type(sliced).__name__ if sliced else 'None')
22print('copy result:', type(copied).__name__ if copied else 'None')
copy error: AttributeError no copy slice result: list copy result: None
r1-p003 · Mutable default and aliasing
1def f(x=None):
2 if x is None:
3 x = []
4 y = x[:]
5 y.append(99)
6 return x, y
7a, b = f()
8print('first call:', a, b)
9c, d = f()
10print('second call:', c, d)
first call: [] [99] second call: [] [99]
r1-p004 · Parameter kinds and defaults
1def process(a, /, b, *, c=None):
2 if c is None:
3 c = b[:]
4 c.append(len(b))
5 return a, b, c
6out = process(1, [2, 3], c=[4])
7print('out:', out)
out: (1, [2, 3], [4, 2])
r1-p005 · Dict ordering and overrides
1class Key:
2 def __init__(self, val):
3 self.val = val
4 def __hash__(self):
5 return hash(self.val)
6 def __eq__(self, other):
7 return self.val == other.val
8k1 = Key(1)
9k2 = Key(1)
10d = {k1: 'first', k2: 'second'}
11items = list(d.items())[:]
12print('dict len:', len(d), 'slice len:', len(items))
13for k, v in items:
14 print(k.val, v)
dict len: 1 slice len: 1 1 second
r1-p006 · Async interaction
1async def gen():
2 for i in [1, 2, 3]:
3 yield i
4async def main():
5 g = [x async for x in gen()] # materialize async gen
6 slice_part = g[:]
7 method_part = g.copy()
8 print('slice:', slice_part, 'type:', type(slice_part).__name__)
9 print('copy:', method_part, 'type:', type(method_part).__name__)
10import asyncio
11asyncio.run(main())
--- orig.py 2026-10-17 17:32:47.514914824 -0400 +++ fixed.py 2026-10-17 17:32:47.811910782 -0400 @@ -3,7 +3,7 @@ yield i async def main(): g = [x async for x in gen()] # materialize async gen - slice_part = g[:] + slice_part = g.copy() method_part = g.copy() print('slice:', slice_part, 'type:', type(slice_part).__name__) print('copy:', method_part, 'type:', type(method_part).__name__)
slice: [1, 2, 3] type: list copy: [1, 2, 3] type: list
r1-p007 · File write truncation
1data = b'hello'
2with open('test.bin', 'wb') as f:
3 f.write(data[:]) # rule does not trigger on bytes
4with open('test.bin', 'rb') as f:
5 print(f.read())
6import os
7os.remove('test.bin')
b'hello'
r1-p008 · Exceptions and finally
1class BadList(list):
2 def __getitem__(self, key):
3 if key == 0:
4 raise ValueError('bad')
5 return super().__getitem__(key)
6 def copy(self):
7 return super().copy()
8b = BadList([1, 2, 3])
9try:
10 x = b[:] # triggers on first element access during slice
11 print('slice succeeded:', x)
12except ValueError as e:
13 print('slice error:', type(e).__name__, str(e))
14try:
15 y = b.copy()
16 print('copy succeeded:', y)
17except ValueError as e:
18 print('copy error:', type(e).__name__, str(e))
slice succeeded: [1, 2, 3] copy succeeded: [1, 2, 3]
r2-p001 · Conditional expression mutation timing (generated program is not deterministic)
1state = []
2def get_target(): return state[:]
3state.append(1)
4a = [] if False else get_target()
5print('result id', id(a))
6print('source len', len(state))
r2-p002 · Identity check reliance
1src = [1, 2]
2candidate = src[:]
3is_same_obj = candidate is src
4print('distinct?', not is_same_obj)
5# rule triggers on candidate = src[:]
6if not is_same_obj:
7 print('ok')
8else:
9 print('unexpected alias')
--- orig.py 2026-10-17 17:33:01.807722136 -0400 +++ fixed.py 2026-10-17 17:33:01.970719959 -0400 @@ -1,5 +1,5 @@ src = [1, 2] -candidate = src[:] +candidate = src.copy() is_same_obj = candidate is src print('distinct?', not is_same_obj) # rule triggers on candidate = src[:]
distinct? True ok