fixhunt

ruff FLY002

labelled unknown
Runs / 2026-10-17T20-46-25-989Z-q9wc-ruff-FLY002ruff 0.17.12026-10-17 20:46 UTC8 programs × 2 round(s)16 programs, 7 fired, 0 fixed, 0 findings, 1 rejectedmodel cost $0.0053

Analysis Nemotron 3 Super

Flags str.join() calls where all arguments are simple variables or literals and suggests replacing them with equivalent f-strings.

Fix: Rewrites str.join(separator, iterable) into an f-string where each element from the iterable is placed literally separated by the separator, preserving order and literal separator content.

10 hazards
  • If any joined element is not a string, the f-string will invoke its __format__ method (default str) while str.join requires actual strings, potentially changing output or raising AttributeError.
  • If an element defines a custom __format__ that differs from its __str__, the f-string output will diverge from the join result.
  • If evaluating any element has side effects (e.g., function calls), the f-string evaluates them once during formatting, same as join, but if the join argument is an iterator consumed elsewhere, behavior may differ due to consumption timing.
  • If the separator contains replacement field syntax like '{' or '}', the f-string will interpret it as a format specifier, altering output or causing ValueError.
  • If the iterable is not a tuple/list (e.g., set, dict_keys), the f-string preserves iteration order but join may have used arbitrary order (for sets), leading to non-deterministic output changes.
  • If any element equals None, str.join raises TypeError while f-string converts it to 'None', changing behavior from exception to string output.
  • If the iterable is empty, both produce empty string, but if the separator is invalid in an f-string context (e.g., unbalanced braces), the f-string may raise ValueError where join succeeds.
  • Comments inside the join call parentheses or on the same line may be lost during rewrite, as the autofix replaces the entire expression without preserving internal comments.
  • If the join call is part of a larger expression and the f-string introduces unintended line breaks or changes precedence due to formatting, it could alter grouping.
  • If any element is an object with a __format__ method that returns non-string (e.g., bytes), the f-string result may be bytes while join expects strings, causing type errors downstream.

Programs

idresulttitlehazard
r1-p001silent prediction offSide effect evaluation consistencyIf evaluating any element has side effects (e.g., function calls), the f-string evaluates them once during formatting, same as join, but if the join argument is an iterator consumed elsewhere, behavio
r1-p002fired, no fixPrecedence change in larger expressionIf the join call is part of a larger expression and the f-string introduces unintended line breaks or changes precedence due to formatting, it could alter grouping.
r1-p003fired, no fixNon-string elements and __format__ vs str()If any joined element is not a string, the f-string will invoke its __format__ method (default str) while str.join requires actual strings, potentially changing output or raising AttributeError.
r1-p004silent prediction offMutable defaults and aliasingmutable default arguments and aliasing after a rewrite
r1-p005invalidSet iteration order non-determinismIf the iterable is not a tuple/list (e.g., set, dict_keys), the f-string preserves iteration order but join may have used arbitrary order (for sets), leading to non-deterministic output changes.
r1-p006silent prediction offGenerators consumed once vs eagergenerators vs lists, iterators consumed once, lazy vs eager evaluation
r1-p007fired, no fixExceptions in rewritten code and finally/elseexceptions raised inside the rewritten code, finally/else clauses, exception chaining
r1-p008silent prediction offAsync def/await ordering and async generatorsasync def/await ordering, async generators
r2-p001silent prediction offWalrus and comprehension scopingwalrus, comprehensions scoping, late binding in closures and lambdas
r2-p002fired, no fixNone element behavior differenceIf any element equals None, str.join raises TypeError while f-string converts it to 'None', changing behavior from exception to string output.
r2-p003fired, no fixCustom __format__ divergenceIf an element defines a custom __format__ that differs from its __str__, the f-string output will diverge from the join result.
r2-p004silent prediction offMutable default arguments and aliasingmutable default arguments and aliasing after a rewrite
r2-p005silent prediction offSeparator with brace syntaxIf the separator contains replacement field syntax like '{' or '}', the f-string will interpret it as a format specifier, altering output or causing ValueError.
r2-p006fired, no fixShadowed builtinsshadowed builtins and names redefined later in the module
r2-p007silent prediction offEmpty iterable with invalid separator bracesIf the iterable is empty, both produce empty string, but if the separator is invalid in an f-string context (e.g., unbalanced braces), the f-string may raise ValueError where join succeeds.
r2-p008fired, no fix__format__ returns non-stringIf any element is an object with a __format__ method that returns non-string (e.g., bytes), the f-string result may be bytes while join expects strings, causing type errors downstream.

Sources

r1-p001 · Side effect evaluation consistency
1def get_a():
2 print('A')
3 return 'a'
4def get_b():
5 print('B')
6 return 'b'
7s = ' '
8trace = []
9trace.append(s.join((get_a(), get_b())))
10trace.append(f'{get_a()} {get_b()}')
11print('|'.join(trace))
A
B
A
B
a b|a b
r1-p002 · Precedence change in larger expression
1a = 'hello'
2b = 'world'
3expr = '-' + ' '.join((a, b)) + '-'
4fexpr = '-' + f'{a} {b}' + '-'
5print(expr == fexpr)
6# Force different grouping via missing parentheses
7x = 1
8y = 2
9z = 3
10bad_join = '' + ''.join((str(x), str(y))) + str(z)
11bad_fstring = '' + f'{x}{y}' + str(z)
12print(bad_join, bad_fstring, sep='|')
True
123|123
r1-p003 · Non-string elements and __format__ vs str()
1class NotStr:
2 def __init__(self, val):
3 self.val = val
4 def __str__(self):
5 return f'[{self.val}]'
6 def __format__(self, fmt):
7 return f'<{self.val}>'
8a = NotStr('x')
9b = NotStr('y')
10try:
11 res = ''.join((a, b))
12except Exception as e:
13 res = f'{type(e).__name__}: {e}'
14try:
15 fres = f'{a}{b}'
16except Exception as e:
17 fres = f'{type(e).__name__}: {e}'
18print(f'join:{res}|fstring:{fres}')
join:TypeError: sequence item 0: expected str instance, NotStr found|fstring:<x><y>
r1-p004 · Mutable defaults and aliasing
1def make_pair(lst=None):
2 if lst is None:
3 lst = []
4 lst.extend(['a', 'b'])
5 return lst
6pair1 = make_pair()
7pair2 = make_pair()
8sep = ' '
9joined = sep.join(pair1)
10fstringed = f'{pair1[0]} {pair1[1]}'
11print(f'{joined}|{fstringed}')
12# Show aliasing
13pair1.clear()
14print(len(pair2))
a b|a b
2
r1-p005 · Set iteration order non-determinism (generated program is not deterministic)
1s = {'b', 'a'}
2joined = ''.join(s)
3fstringed = ''.join([f'{c}' for c in s]) # simulate f-string order
4print(f'join:{joined}|fstring_sim:{fstringed}')
5# To see difference, we need fixed iteration vs random; but sets are unordered.
6# Instead show that f-string forces list order while join uses set's arbitrary order.
7# We'll run twice and see if join differs.
8results = []
9for _ in range(2):
10 s = {'b', 'a'}
11 results.append(''.join(s))
12print(f'join varies: {results[0] != results[1]}')
r1-p006 · Generators consumed once vs eager
1def gen():
2 yield 'x'
3 yield 'y'
4g = gen()
5first = ''.join(g)
6try:
7 second = ''.join(g)
8except:
9 second = '<exhausted>'
10# Reset
11g = gen()
12fstring_first = f'{next(g)}{next(g)}'
13print(f'join1:{first}|join2:{second}|fstring:{fstring_first}')
join1:xy|join2:|fstring:xy
r1-p007 · Exceptions in rewritten code and finally/else
1def fails():
2 raise ValueError('bad')
3try:
4 result = ':'.join(('ok', fails()))
5except Exception as e:
6 join_ex = f'{type(e).__name__}: {e}'
7try:
8 result = f'{fails()}:ok'
9except Exception as e:
10 fstring_ex = f'{type(e).__name__}: {e}'
11print(f'join:{join_ex}|fstring:{fstring_ex}')
12# Finally clause
13try:
14 x = 1
15 _ = ':'.join((str(x), fails()))
16except:
17 pass
18else:
19 print('no_exc_join', end='')
20finally:
21 print('_finally_join')
22try:
23 x = 1
24 _ = f'{x}:{fails()}'
25except:
26 pass
27else:
28 print('no_exc_fstring', end='')
29finally:
30 print('_finally_fstring')
join:ValueError: bad|fstring:ValueError: bad
_finally_join
_finally_fstring
r1-p008 · Async def/await ordering and async generators
1import asyncio
2async def afunc():
3 await asyncio.sleep(0.001)
4 return 'done'
5async def main():
6 r1 = ' '.join((await afunc(), await afunc()))
7 r2 = f'{await afunc()} {await afunc()}'
8 print(f'{r1}|{r2}')
9asyncio.run(main())
done done|done done
r2-p001 · Walrus and comprehension scoping
1def f():
2 x = 1
3 g = lambda: " ".join((y := x, x + 1))
4 x = 2
5 return g()
6print(f())
r2-p002 · None element behavior difference
1a = None
2b = 'hello'
3try:
4 result = ' '.join((a, b))
5except Exception as e:
6 print(f'{type(e).__name__}: {e}')
7else:
8 print(repr(result))
9# f-string version would print: 'None hello'
TypeError: sequence item 0: expected str instance, NoneType found
r2-p003 · Custom __format__ divergence
1class Custom:
2 def __str__(self):
3 return 'str'
4 def __format__(self, spec):
5 return f'fmt[{spec}]'
6c = Custom()
7print('|'.join((c,))) # -> 'str'
8print(f'{c}|') # -> 'fmt[]' (different)
9# Rule would suggest f'{c}' which changes meaning
r2-p004 · Mutable default arguments and aliasing
1def append_join(x, lst=[]):
2 lst.append(x)
3 return '-'.join((lst[0], lst[-1]))
4print(append_join(1)) # ['1'] -> '1-1'
5print(append_join(2)) # ['1', '2'] -> '1-2'
6# After f-string fix: f'{lst[0]}-{lst[-1]}' still uses current list, so same?
r2-p005 · Separator with brace syntax
1sep = '{name}'
2a = 'Alice'
3b = 'Bob'
4joined = sep.join((a, b))
5print(repr(joined)) # 'Alice{name}Bob'
6# F-string would try: f'Alice{name}Bob' -> NameError if name undefined, or value if defined
7# To make deterministic: define name
8name = 'X'
9fstring_result = f'{a}{sep}{b}'
10print(f'F-string gives: {repr(fstring_result)}') # 'Alice{X}Bob' != 'Alice{name}Bob'
'Alice{name}Bob'
F-string gives: 'Alice{name}Bob'
r2-p006 · Shadowed builtins
1str = lambda x: f'[{x}]'
2try:
3 result = ''.join((str, str)) # str is now lambda, not builtin
4except Exception as e:
5 print(f'{type(e).__name__}: {e}')
6else:
7 print(repr(result))
8# After rebuild: str is reassigned below, but closure captures outer?
9# Actually: at join time, str is lambda -> TypeError: join expects str, got function
TypeError: sequence item 0: expected str instance, function found
r2-p007 · Empty iterable with invalid separator braces
1sep = '{'
2items = []
3joined = sep.join(items)
4print(repr(joined)) # ''
5try:
6 # Simulate what f-string would do: f'' (no content) but sep has {
7 # Actually, f-string wouldn't use sep if empty? Hazard: if rule tries to make f'' and puts sep inside?
8 # Assume rule does: for empty, f'' but if separator has {, it might still be part of template
9 # But with no elements, f-string would be just '' regardless of sep?
10 # To make sep matter: rule might incorrectly do f'{sep}' even when empty? Not per spec.
11 # Instead: hazard says if separator invalid in f-string context.
12 # Let's make a case where separator alone causes f-string error.
13 dummy = f'{sep}' # This line alone would raise ValueError: unsupported format specifier
14 print('This should not print')
15except Exception as e:
16 print(f'{type(e).__name__}: {e}')
17else:
18 print(repr(dummy))
''
This should not print
'{'
r2-p008 · __format__ returns non-string
1class BytesReturn:
2 def __format__(self, spec):
3 return b'bytes'
4 def __str__(self):
5 return 'str'
6obj = BytesReturn()
7try:
8 j = '|'.join((obj,))
9 print(f'Join result: {repr(j)}') # -> 'str'
10except Exception as e:
11 print(f'Join error: {type(e).__name__}: {e}')
12try:
13 fs = f'{obj}|' # -> b'bytes|' (bytes)
14 print(f'F-string result: {repr(fs)}')
15 # Downstream: if used where string expected
16 print(fs.upper()) # AttributeError: bytes has no upper
17except Exception as e:
18 print(f'F-string downstream: {type(e).__name__}: {e}')
Join error: TypeError: sequence item 0: expected str instance, BytesReturn found
F-string downstream: TypeError: __format__ must return a str, not bytes