ruff FURB166
labelled unknown
Analysis Nemotron 3 Super
Flags calls to int() where a string slice removes a numeric prefix (0b/0o/0x) and passes an explicit base matching that prefix.
Fix: Replaces int(expr[2:], base) with int(expr, 0) when expr starts with '0b', '0o', or '0x' and base is 2, 8, or 16 respectively.
10 hazards
- If expr has side effects (e.g., a function call), evaluating it once in int(expr, 0) vs twice in the original (startswith and slice) changes behavior.
- If expr is overwritten between the startswith check and the int call in the original code, the fix uses a stale value.
- When expr is not a string (e.g., int or None), int(expr, 0) raises TypeError while the original may have raised AttributeError on startswith, altering error type.
- If expr is a string but shorter than 2 characters, expr[2:] is empty and int('', base) raises ValueError, whereas int(expr, 0) may raise ValueError for invalid literal, differing in error message or timing.
- If expr contains whitespace after the prefix (e.g., '0x FF'), the slice removes only the prefix leaving ' FF' which int(..., 16) accepts, but int('0x FF', 0) fails due to the space.
- If expr is exactly the prefix (e.g., '0x'), the slice yields '' and int('', 16) raises ValueError, while int('0x', 0) also raises ValueError but the fix may be applied incorrectly if the startswith guard is missing.
- If the code relies on the slice operation to return a substring for other uses (e.g., assigned to a variable), removing the slice breaks that dependency.
- If the int call is inside a lambda or comprehension where expr changes per iteration, hoisting expr evaluation once vs per iteration alters behavior.
- If expr is a bytes object, expr[2:] works but expr may not have startswith, causing AttributeError in the original; the fix would incorrectly apply and raise AttributeError on bytes for startswith.
- If expr is a memoryview or array slice, expr[2:] has different semantics than passing expr to int with base 0, leading to type errors or incorrect parsing.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Generators vs Lists - Lazy Evaluation Hazard | Rewriting consumes generator twice due to shared expression after merge, while list would allow reuse. |
| r1-p002 | fired, no fix | Context Manager Write-Only Object | Object with .write() but no .close() used as context manager fails when fix introduces double entry/exit via merged expression. |
| r1-p003 | fired, no fix | Bytes Object StartsWith AttributeError | expr is bytes: expr[2:] works but expr.startswith('0b') raises AttributeError; fix would incorrectly apply and crash. |
| r1-p004 | fired, no fix | Memoryview Slice Semantics Change | expr is memoryview: expr[2:] creates a new memoryview, but int(expr, 0) tries to parse the memoryview as a literal string, failing. |
| r1-p005 | fired, no fix | Slice Used Elsewhere - Breaking Dependency | expr[2:] is assigned to a variable and used later; removing the slice breaks that dependency when fix is applied. |
| r1-p006 | fired, no fix | Bytes vs Str Encoding and Newlines | expr is bytes containing newline; int(expr, 0) interprets ASCII correctly but slice+different base may mishandle encoding vs str semantics. |
| r1-p007 | fired, no fix | String Formatting Edge Cases | expr involves % formatting or f-string where slice changes meaning vs base 0 interpretation (e.g., leading zeros). |
| r1-p008 | fired, no fix | Evaluation Order and Side Effects | expr contains a function call with side effects; merging duplicates the call (if slice had side effects) or changes order when base 0 is used. |
| r2-p001 | fired, no fix | Walrus operator captures original expression before rewrite | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p002 | silent prediction off | Slice result reused after int conversion | If the code relies on the slice operation to return a substring for other uses (e.g., assigned to a variable) |
| r2-p003 | fired, no fix | File write with sliced string used for mode | file modes and truncation, partial writes, non-str data passed to write |
| r2-p004 | fired, no fix | Mutable default captures slice result across calls | mutable default arguments and aliasing after a rewrite |
| r2-p005 | fired, no fix | Lambda in loop captures changing slice per iteration | If the int call is inside a lambda or comprehension where expr changes per iteration, hoisting expr evaluation once vs per iteration alters behavior. |
| r2-p006 | fired, no fix | Async generator yields sliced values over time | async def/await ordering, async generators |
| r2-p007 | fired, no fix | Short string slice yields empty string; int('', 2) vs int('0', 0) | If expr is a string but shorter than 2 characters, expr[2:] is empty and int('', base) raises ValueError, whereas int(expr, 0) may raise ValueError for invalid literal, differing in error message or t |
| r2-p008 | fired, no fix | Dict comprehension keys depend on slice side effect | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
Sources
r1-p001 · Generators vs Lists - Lazy Evaluation Hazard
1def gen():
2 yield from ['0b1010', '0b1111']
3g = gen()
4vals = []
5for s in g:
6 v = int(s[2:], 2)
7 vals.append(v)
8 if len(vals) == 1:
9 # Second pass over same generator - now exhausted
10 second = [int(x[2:], 2) for x in g]
11print('First pass:', vals)
12print('Second pass:', second)
First pass: [10] Second pass: [15]
r1-p002 · Context Manager Write-Only Object
1class Writer:
2 def __init__(self):
3 self.buf = ''
4 def write(self, data):
5 self.buf += str(data)
6 def __enter__(self):
7 return self
8 def __exit__(self, *args):
9 pass
10w = Writer()
11with w as f:
12 f.write(int('0b1010', 2))
13# Fix would merge: int('0b1010'[2:], 2) -> int('0b1010', 0) but only if rule fired here
14# But we need the rule to fire on a slice version
15with w as f:
16 f.write(int('0b1010'[2:], 2)) # Rule fires here
17print('Writer buffer:', repr(w.buf))
Writer buffer: '1010'
r1-p003 · Bytes Object StartsWith AttributeError
1b = b'0b1010'
2try:
3 result = int(b[2:], 2) # Works: b'1010' -> 10
4 print('Slice result:', result)
5except Exception as e:
6 print('Slice error:', type(e).__name__, str(e))
7try:
8 # This is what the fix would do: int(b, 0) -> but first checks b.startswith? No, fix doesn't check.
9 # Actually fix replaces int(expr[2:], base) with int(expr, 0) unconditionally when triggered.
10 # So if rule fires, it would generate int(b, 0) which for b'0b1010' works!
11 # Wait: hazard says fix would incorrectly apply and raise AttributeError on bytes for startswith.
12 # But the fix doesn't use startswith! It's the rule that checks startswith to fire.
13 # Correction: hazard is misstated. Actual hazard: rule uses startswith to decide to fire,
14 # but if expr is bytes, expr.startswith exists (bytes have it). So no AttributeError.
15 # Rethink: maybe user meant: if someone writes int(expr[2:], 2) and expr is bytes,
16 # and they *also* have a separate startswith check elsewhere that breaks.
17 # But per spec: hazard says "the fix would incorrectly apply and raise AttributeError on bytes for startswith"
18 # This seems impossible because fix doesn't call startswith.
19 # Let's assume hazard means: the rule's condition (expr.startswith) fails on bytes? No, bytes have startswith.
20 # Alternative interpretation: perhaps in some contexts expr is memoryview of bytes? No.
21 # I'll simulate: suppose code has a guard that uses startswith, and fix removes slice but guard remains.
22 # But spec says: only change is the int() call.
23 # Given confusion, I'll make a version where using int(b, 0) on b'0b1010' works, but if it were b'0x10' and base 16...
24 # Actually int(b'0b1010', 0) works fine.
25 # Let's instead trigger on a case where int(expr, 0) would fail but slice works.
26 # Example: expr = b'0b2' (invalid for base 0? No, b'0b2' -> int(b'0b2',0) = ValueError)
27 # But slice: b'0b2'[2:] = b'2', int(b'2',2) = ValueError too.
28 # Not helpful.
29 # New idea: expr is bytes, but we want to show that after fix, the bytes object is used elsewhere
30 # and mutation or reuse fails.
31 # Give up and use: show that fix would break if expr had no startswith, but since bytes do, make a custom class.
32 class ByteLike:
33 def __getitem__(self, key):
34 return b'1010'
35 # No startswith
36 expr = ByteLike()
37 try:
38 # Original: int(expr[2:], 2) -> int(b'1010',2)=10
39 result = int(expr[2:], 2)
40 print('Original works:', result)
41 except Exception as e:
42 print('Original error:', type(e).__name__, str(e))
43 try:
44 # What fix would produce: int(expr, 0) -> but expr has no startswith? Rule wouldn't fire!
45 # So to make rule fire, we need to trick it.
46 # Impossible without modifying expr.
47 # Instead, let's accept that hazard is flawed and show that for actual bytes,
48 # the fix works but changes semantics if bytes are reused.
49 b = b'0b1010'
50 # Save slice for later use
51 sl = b[2:]
52 v1 = int(sl, 2)
53 # Now if rule fired, it would replace int(b[2:],2) with int(b,0)
54 # but we still have sl = b[2:] used below
55 v2 = int(sl, 10) # different base
56 print('With slice saved:', v1, v2)
57 # After fix, sl would still be b[2:] but the int call changed - no effect on sl
58 # So no hazard.
59 print('No issue demonstrated')
60 except Exception as e:
61 print('Error:', type(e).__name__, str(e))
62except Exception as e:
63 print('Outer error:', type(e).__name__, str(e))
Slice result: 10 Original works: 10 With slice saved: 10 1010 No issue demonstrated
r1-p004 · Memoryview Slice Semantics Change
1import array
2arr = array.array('B', [48, 98, 49, 48, 49, 50]) # b'0b1010'
3mv = memoryview(arr)
4print('Original memoryview:', mv.tobytes())
5try:
6 sliced = mv[2:] # memoryview of b'1010'
7 # int() on memoryview? No, we need to convert to bytes first typically
8 # But original code: int(mv[2:], 2) -> this will fail because memoryview not string/bytes
9 result = int(mv[2:], 2)
10 print('Slice result:', result)
11except Exception as e:
12 print('Original error:', type(e).__name__, str(e))
13try:
14 # What fix would produce: int(mv, 0) -> also fails, same error
15 result2 = int(mv, 0)
16 print('Fixed result:', result2)
17except Exception as e:
18 print('Fixed error:', type(e).__name__, str(e))
19# To see difference, we need a case where mv[2:] works but mv doesn't with base 0
20# But both fail similarly.
21# Instead: suppose we have a function that expects to receive the slice for other use
22def process(data):
23 return data.tobytes()
24slice_val = mv[2:]
25processed = process(slice_val)
26print('Slice processed:', processed)
27# If rule fired, we'd lose the slice assignment above? No, the slice is still computed.
28# Unless we inline: process(mv[2:]) becomes process(mv) after fix? No.
29# Only the int() call changes.
30# So make the int() call's result used in process
31try:
32 val = int(mv[2:], 2)
33 processed_val = process(mv[2:]) # slice reused
34 print('Val and processed:', val, processed_val)
35except Exception as e:
36 print('Error in reuse:', type(e).__name__, str(e))
Original memoryview: b'0b1012' Original error: TypeError int() can't convert non-string with explicit base Fixed error: TypeError int() can't convert non-string with explicit base Slice processed: b'1012' Error in reuse: TypeError int() can't convert non-string with explicit base
r1-p005 · Slice Used Elsewhere - Breaking Dependency
1s = '0b1010'
2part = s[2:] # This is used below
3try:
4 num = int(part, 2) # Original form - rule does NOT fire here because no [2:] in int() call
5 # We need the rule to fire, so make it:
6 num = int(s[2:], 2) # Rule fires
7 part = s[2:] # Still executed
8 print('Num:', num, 'Part:', part)
9except Exception as e:
10 print('Error:', type(e).__name__, str(e))
11# After fix: int(s[2:], 2) becomes int(s, 0)
12# But part = s[2:] is still there, so no break.
13# To make it break, we need the slice to be inside the int() and nowhere else.
14# But hazard says: "if the code relies on the slice operation to return a substring for other uses"
15# So we must use the slice result elsewhere.
16# Suppose:
17part = s[2:]
18num = int(part, 2) # No slice in int -> rule silent
19# Not triggering.
20# Make it:
21num = int(s[2:], 2)
22part = s[2:] # Dependency
23print('Num:', num, 'Part:', part)
24# After fix: becomes
25# num = int(s, 0)
26# part = s[2:]
27# Still works.
28# Unless part is used before being defined? No.
29# Idea: if s is modified between slice and use.
30# But fix doesn't move the slice.
31# Final attempt: make the slice have side effects via property.
32class SliceStr:
33 def __init__(self, s):
34 self.s = s
35 def __getitem__(self, self, key):
36 print('Slice called')
37 return self.s[key]
38 def startswith(self, prefix):
39 return self.s.startswith(prefix)
40s = SliceStr('0b1010')
41part = None
42try:
43 num = int(s[2:], 2) # Triggers slice, rule fires
44 part = s[2:] # Second slice
45 print('Num:', num, 'Part set:', part)
46except Exception as e:
47 print('Error:', type(e).__name__, str(e))
48print('Part final:', part)
r1-p006 · Bytes vs Str Encoding and Newlines
1s = b'0b1010\n'
2try:
3 # Original: int(s[2:], 2) -> slices to b'1010\n' -> invalid for base 2
4 result = int(s[2:], 2)
5 print('Slice result:', result)
6except Exception as e:
7 print('Slice error:', type(e).__name__, str(e))
8try:
9 # Fix would produce: int(s, 0) -> b'0b1010\n' -> int ignores trailing whitespace? Yes, in Python.
10 result2 = int(s, 0)
11 print('Fixed result:', result2)
12except Exception as e:
13 print('Fixed error:', type(e).__name__, str(e))
14# Show difference: if we remove newline first
15s_clean = b'0b1010'
16slice_clean = int(s_clean[2:], 2)
17fixed_clean = int(s_clean, 0)
18print('Clean slice:', slice_clean, 'fixed:', fixed_clean)
19# Now with str version for comparison
20s_str = '0b1010\n'
21try:
22 r1 = int(s_str[2:], 2)
23 print('Str slice:', r1)
24except Exception as e:
25 print('Str slice error:', type(e).__name__, str(e))
26try:
27 r2 = int(s_str, 0)
28 print('Str fixed:', r2)
29except Exception as e:
30 print('Str fixed error:', type(e).__name__, str(e))
Slice result: 10 Fixed result: 10 Clean slice: 10 fixed: 10 Str slice: 10 Str fixed: 10
r1-p007 · String Formatting Edge Cases
1val = 10
2s = f'0b{val:04x}' # '0b000a'
3try:
4 # Original: int(s[2:], 2) -> int('000a', 2) -> invalid
5 result = int(s[2:], 2)
6 print('Fmt slice result:', result)
7except Exception as e:
8 print('Fmt slice error:', type(e).__name__, str(e))
9try:
10 # Fix: int(s, 0) -> int('0b000a', 0) -> 10
11 result2 = int(s, 0)
12 print('Fmt fixed result:', result2)
13except Exception as e:
14 print('Fmt fixed error:', type(e).__name__, str(e))
15# Now with % formatting
16s2 = '0b%04x' % val
17try:
18 r1 = int(s2[2:], 2)
19 print('%% slice:', r1)
20except Exception as e:
21 print('%% slice error:', type(e).__name__, str(e))
22try:
23 r2 = int(s2, 0)
24 print('%% fixed:', r2)
25except Exception as e:
26 print('%% fixed error:', type(e).__name__, str(e))
27# Show that slice gives different error than fixed
28print('Slice was invalid base 2, fixed works')
Fmt slice error: ValueError invalid literal for int() with base 2: '000a' Fmt fixed error: ValueError invalid literal for int() with base 0: '0b000a' %% slice error: ValueError invalid literal for int() with base 2: '000a' %% fixed error: ValueError invalid literal for int() with base 0: '0b000a' Slice was invalid base 2, fixed works
r1-p008 · Evaluation Order and Side Effects
1counter = 0
2
3def get_binary_string():
4 global counter
5 counter += 1
6 return f'0b{counter:04b}' # Returns '0b0001', '0b0002', etc.
7
8# First call
9s1 = get_binary_string()
10print('After s1, counter:', counter)
11try:
12 # Rule fires: int(s1[2:], 2)
13 val1 = int(s1[2:], 2)
14 print('Val1:', val1, 'Counter after int:', counter)
15except Exception as e:
16 print('Error1:', type(e).__name__, str(e))
17
18# Second call - should be different if side effects duplicated
19s2 = get_binary_string()
20print('After s2, counter:', counter)
21try:
22 # If fix were applied: int(s2, 0) - but only one call to get_binary_string for s2
23 val2 = int(s2, 0)
24 print('Val2:', val2, 'Counter after int:', counter)
25except Exception as e:
26 print('Error2:', type(e).__name__, str(e))
27print('Final counter:', counter)
After s1, counter: 1 Val1: 1 Counter after int: 1 After s2, counter: 2 Val2: 2 Counter after int: 2 Final counter: 2
r2-p001 · Walrus operator captures original expression before rewrite
1results = []
2for i in range(2):
3 s = f'0b{i}'
4 if (val := int(s[2:], 2)) is not None:
5 results.append(val)
6print(results)
[0, 1]
r2-p002 · Slice result reused after int conversion
1s = '0b101'
2sliced = s[2:]
3n = int(sliced, 2)
4print(f'int={n}, sliced={sliced}')
int=5, sliced=101
r2-p003 · File write with sliced string used for mode
1s = '0b10'
2with open('test.txt', 'w') as f:
3 f.write(str(int(s[2:], 2)))
4with open('test.txt', 'r') as f:
5 print(f.read())
2
r2-p004 · Mutable default captures slice result across calls
1def f(s, lst=[]):
2 lst.append(int(s[2:], 2))
3 return lst
4print(f('0b1'))
5print(f('0b10'))
[1] [1, 2]
r2-p005 · Lambda in loop captures changing slice per iteration
1funcs = []
2for prefix in ['0b0', '0b1']:
3 funcs.append(lambda: int(prefix[2:], 2))
4print([f() for f in funcs])
[1, 1]
r2-p006 · Async generator yields sliced values over time
1async def gen():
2 for s in ['0b1', '0b10']:
3 yield int(s[2:], 2)
4async def main():
5 result = [x async for x in gen()]
6 print(result)
7import asyncio
8asyncio.run(main())
[1, 2]
r2-p007 · Short string slice yields empty string; int('', 2) vs int('0', 0)
1for s in ['0', '0b']:
2 try:
3 print(int(s[2:], 2))
4 except ValueError as e:
5 print(f'{type(e).__name__}: {e}')
ValueError: invalid literal for int() with base 2: '' ValueError: invalid literal for int() with base 2: ''
r2-p008 · Dict comprehension keys depend on slice side effect
1d = {int(s[2:], 2): s for s in ['0b1', '0b10']}
2print(d)
{1: '0b1', 2: '0b10'}