ruff FLY002
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Side effect evaluation consistency | 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, behavio |
| r1-p002 | fired, no fix | Precedence change in larger expression | 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. |
| r1-p003 | fired, no fix | Non-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-p004 | silent prediction off | Mutable defaults and aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p005 | invalid | Set iteration order non-determinism | 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. |
| r1-p006 | silent prediction off | Generators consumed once vs eager | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r1-p007 | fired, no fix | Exceptions in rewritten code and finally/else | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r1-p008 | silent prediction off | Async def/await ordering and async generators | async def/await ordering, async generators |
| r2-p001 | silent prediction off | Walrus and comprehension scoping | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p002 | fired, no fix | None element behavior difference | If any element equals None, str.join raises TypeError while f-string converts it to 'None', changing behavior from exception to string output. |
| r2-p003 | fired, no fix | Custom __format__ divergence | If an element defines a custom __format__ that differs from its __str__, the f-string output will diverge from the join result. |
| r2-p004 | silent prediction off | Mutable default arguments and aliasing | mutable default arguments and aliasing after a rewrite |
| r2-p005 | silent prediction off | Separator with brace syntax | If the separator contains replacement field syntax like '{' or '}', the f-string will interpret it as a format specifier, altering output or causing ValueError. |
| r2-p006 | fired, no fix | Shadowed builtins | shadowed builtins and names redefined later in the module |
| r2-p007 | silent prediction off | Empty iterable with invalid separator braces | 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. |
| r2-p008 | fired, no fix | __format__ returns non-string | 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. |
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