ruff RUF015
labelled unknown
Analysis Nemotron 3 Super
RUF015 flags eager allocation of iterables when only the first element is accessed via indexing or pop(0), suggesting replacement with lazy next(iter(...)).
Fix: Replace the eager iterable construction and indexing/pop with a lazy next(iter(...)) expression, preserving the inner iterable or comprehension but removing the outer list/tuple/set constructor and the [0] or .pop(0) access.
11 hazards
- Rewriting list(seq)[0] to next(iter(seq)) changes the exception type from IndexError on empty input to StopIteration, breaking exception handlers that catch IndexError but not StopIteration.
- The rewrite eliminates eager evaluation of seq, so any side effects (e.g., I/O, state mutation) during iteration now occur only when the iterator is consumed, potentially delaying or avoiding them entirely if the result is unused.
- If seq is used elsewhere after the original code, the rewrite may alter its state because iter(seq) advances the iterator, whereas list(seq) leaves seq unchanged for reusable iterables like lists.
- For non-reusable iterators (e.g., file objects, generators), the rewrite consumes the iterator permanently, while the original list(seq)[0] would have exhausted it anyway—but if the list was stored or inspected, the rewrite loses that behavior.
- When seq is a list, tuple, or other sequence, list(seq)[0] creates a copy; next(iter(seq)) references the original, so mutations to the original after the call affect the result in the rewrite but not in the original.
- If the comprehension or generator expression inside list(...) contains capture variables that mutate during iteration, the timing of their evaluation shifts, possibly altering values observed due to late-binding closure effects.
- The rewrite may drop trailing comments attached to the closing bracket or pop(0) call, as autofix tools often discard whitespace and comments during tree reconstruction.
- In code like f(list(seq)[0]), rewriting to f(next(iter(seq))) changes evaluation order: seq is no longer evaluated before f is called, but during f’s execution if it consumes the iterator, altering side-effect sequencing.
- If seq is a property or function call with side effects, the rewrite may invoke it multiple times if next(iter(...)) is used in a context that re-evaluates the expression (e.g., in a loop), whereas the original computed it once.
- For expressions like [i for i in seq if cond][0], the rewrite to next(i for i in seq if cond) is valid, but if cond has side effects, their timing changes relative to other code between the comprehension and the indexing.
- When the iterable is a class instance with a custom __iter__ that returns a mutable iterator, sharing that iterator via list(...) vs. iter(...) can lead to aliasing issues if the iterator is retained elsewhere.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Property with side effects - multiple invocation | If seq is a property or function call with side effects, the rewrite may invoke it multiple times if next(iter(...)) is used in a context that re-evaluates the expression. |
| r1-p002 | fired, no fix | Trailing comment loss | The rewrite may drop trailing comments attached to the closing bracket or pop(0) call, as autofix tools often discard whitespace and comments during tree reconstruction. |
| r1-p003 | fired, no fix | State alteration of reusable iterable | If seq is used elsewhere after the original code, the rewrite may alter its state because iter(seq) advances the iterator, whereas list(seq) leaves seq unchanged for reusable iterables like lists. |
| r1-p004 | fired, no fix | Keyword-only and positional-only forwarding with defaults | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p005 | fired, no fix | Evaluation order and side effects in merged expressions | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r1-p006 | silent | Star-imports and module-level side effects | star-imports, module-level side effects, __all__ |
| r1-p007 | fired, no fix | Numeric coercion and rounding | int/float/bool coercion, Decimal, division and rounding |
| r1-p008 | fired, no fix | Type annotations evaluated at runtime | type annotations evaluated at runtime, typing-only imports |
| r2-p001 | fired, no fix | Mutation after list() affects result | When seq is a mutable sequence, list(seq)[0] copies elements; next(iter(seq)) returns a reference to the original element. Mutating the original sequence after the call affects the result in the rewri |
| r2-p002 | fired, no fix | Exception in finally block with chaining | If an exception is raised inside the rewritten expression and caught, finally/else clauses and exception chaining (__context__) may differ due to timing of evaluation. |
| r2-p003 | fired, no fix | Side effects delayed by lazy evaluation | The rewrite eliminates eager evaluation of seq, so side effects (e.g., print, mutation) during iteration now occur only when the iterator is consumed, potentially delaying or avoiding them if the resu |
| r2-p004 | fired, no fix | Generator consumed once: exhaustion difference | For generators, the original list(seq)[0] exhausts the generator (via list builder), same as rewrite. But if the list was stored or inspected later, the rewrite loses that behavior — here we store the |
| r2-p005 | invalid | Shadowed built-in 'list' later redefined | If the built-in 'list' is shadowed by a local variable after the code in question, the rewrite uses the current binding of 'list' at runtime, which may differ from the original built-in if the shadowi |
| r2-p006 | fired, no fix | Conditional with side effects: timing changed | For [i for i in seq if cond][0], the rewrite to next(i for i in seq if cond) changes when cond's side effects occur relative to other code between comprehension and indexing. |
| r2-p007 | fired, no fix | Non-reusable iterator: file object exhaustion | For non-reusable iterators (e.g., file objects), the rewrite consumes the iterator permanently, while the original list(seq)[0] would have exhausted it anyway—but if the list was stored or inspected, |
| r2-p008 | fired, no fix | Truthiness: custom __bool__ vs __len__ | Truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' interacts with the rewrite if the expression is used in a boolean context shortly after, and the rewrite changes what object is produ |
Sources
r1-p001 · Property with side effects - multiple invocation
1class Seq:
2 def __init__(self):
3 self.count = 0
4 @property
5 def value(self):
6 self.count += 1
7 return [10, 20]
8s = Seq()
9for _ in range(2):
10 first = list(s.value)[0] # RUF015 trigger
11 print(f'call:{s.count}, first:{first}')
call:1, first:10 call:2, first:10
r1-p002 · Trailing comment loss
1seq = [5, 6]
2first = list(seq)[0] # keep this comment
3print(first)
5
r1-p003 · State alteration of reusable iterable
1seq = [1, 2, 3]
2first = list(seq)[0] # RUF015 trigger
3rest = list(seq)
4print(f'first:{first}, rest:{rest}')
first:1, rest:[1, 2, 3]
r1-p004 · Keyword-only and positional-only forwarding with defaults
1def f(*, x=None):
2 return [] if x is None else [x]
3def g(a, /, b=[]):
4 return [a] + b
5print(list(f())[0]) # RUF015 trigger
6print(list(g(1))[0]) # RUF015 trigger
r1-p005 · Evaluation order and side effects in merged expressions
1i = 0
2def next_seq():
3 global i
4 i += 1
5 return [i, i*2]
6val = list(next_seq())[0] + list(next_seq())[0] # RUF015 trigger twice
7print(f'i:{i}, val:{val}')
i:2, val:3
r1-p006 · Star-imports and module-level side effects
1# This hazard is about module-level constructs; skipping as it requires external module.
2print('no trigger')
no trigger
r1-p007 · Numeric coercion and rounding
1from decimal import Decimal
2seq = [Decimal('1.5'), Decimal('2.5')]
3first = int(list(seq)[0]) # RUF015 trigger
4print(f'first:{first}')
first:1
r1-p008 · Type annotations evaluated at runtime
1def get_list() -> list[int]:
2 return [42]
3annotated: int = get_list()[0] # Not a trigger; need list()
4first = int(list(get_list())[0]) # RUF015 trigger
5print(f'first:{first}')
first:42
r2-p001 · Mutation after list() affects result
1lst = [99]
2result_old = list(lst)[0]
3lst[0] = 88
4result_new = list(lst)[0]
5print('old:', result_old)
6print('new:', result_new)
7# Rule triggers on: list(lst)[0]
old: 99 new: 88
r2-p002 · Exception in finally block with chaining
1def get_seq():
2 raise ValueError('bad seq')
3try:
4 x = list(get_seq())[0]
5except Exception as e:
6 print('caught:', type(e).__name__, str(e))
7 try:
8 raise RuntimeError('handler failed') from e
9 except Exception as e2:
10 print('chained:', type(e2).__name__, str(e2))
11 if e2.__cause__:
12 print('cause:', type(e2.__cause__).__name__, str(e2.__cause__))
13# Rule triggers on: list(get_seq())[0]
caught: ValueError bad seq chained: RuntimeError handler failed cause: ValueError bad seq
r2-p003 · Side effects delayed by lazy evaluation
1prints = []
2def make_seq():
3 prints.append('start')
4 for i in range(3):
5 prints.append(f'yield {i}')
6 yield i
7 prints.append('end')
8gen = make_seq()
9# Result unused -> no iteration happens in rewrite
10_ = list(gen)[0] # Would trigger, but result used? No: we assign to _ then ignore
11# Actually, we do use it: we assign to _, but then we don't use _ later.
12# To avoid use, we must not assign or use the result.
13# Let's not assign: just express it and discard.
14list(make_seq())[0] # This line: rule triggers, and we do NOT store result
15print('prints so far:', prints)
16# Now force consumption
17list(make_seq())[0] # Second trigger
18print('prints after:', prints)
19# Rule triggers twice: on both list(make_seq())[0]
prints so far: ['start', 'yield 0', 'yield 1', 'yield 2', 'end', 'start', 'yield 0', 'yield 1', 'yield 2', 'end'] prints after: ['start', 'yield 0', 'yield 1', 'yield 2', 'end', 'start', 'yield 0', 'yield 1', 'yield 2', 'end', 'start', 'yield 0', 'yield 1', 'yield 2', 'end']
r2-p004 · Generator consumed once: exhaustion difference
1def gen():
2 yield 1
3 yield 2
4 yield 3
5g = gen()
6# Original: list(g) builds [1,2,3], then [0] -> 1, but g is now exhausted
7lst = list(g) # g exhausted here
8first = lst[0] # Not a trigger: we already called list(g)
9print('first from list:', first)
10print('remaining in g:', list(g)) # []
11# Now reset and test rewrite: this IS a trigger
12g2 = gen()
13first2 = list(g2)[0] # Rule triggers here: eagerly builds list, then [0]
14print('first2:', first2)
15print('remaining in g2:', list(g2)) # [] -> same? Wait: we want to show difference.
16# Actually, in both cases the generator is exhausted by list().
17# To show difference: we need to NOT exhaust in original? Impossible.
18# Instead: show that if we had kept the list, we could inspect more.
19# But rule fires on list(g)[0] — we can't avoid building list.
20# So we change approach: use a case where original does NOT exhaust? No.
21# Hazard says: 'if the list was stored or inspected' — so we store the list in original, but in rewrite we don't.
22# Original: we do list(g) -> store it -> then take [0] -> we still have the list.
23# Rewrite: we only get first element -> no list stored.
24# So:
25g = gen()
26full_list = list(g) # Not a trigger: we mean to keep it
27first_old = full_list[0] # Not a trigger
28print('old first:', first_old)
29print('old rest:', full_list[1:])
30# Now rewrite version: this IS a trigger
31g2 = gen()
32first_new = list(g2)[0] # TRIGGER: list built and immediately indexed
33print('new first:', first_new)
34print('new rest attempt:', list(g2)) # g2 is exhausted by the list() call above!
35# So both exhaust g2 in the list() call. Still same.
36# Correction: the hazard is about the *rewrite* consuming the iterator permanently, while original would have left a list.
37# But in original, we DO build the list (to take [0]), so we also consume it.
38# Unless... the original is not list(seq)[0] but seq[0] for a list? No — hazard specifies list(seq)[0].
39# We must accept: for generators, both forms exhaust the iterator equally when list() is called.
40# The only difference is if the original list was used elsewhere — which we can demonstrate by storing it.
41# So we compare:
42# Version A (original-like): we build list and keep it
43# Version B (rewrite-like): we build list and throw it away after [0]
44# But the rule fires on the expression — we can't control whether we keep the list in the original form.
45# However, the original code pattern is list(seq)[0] — which by definition does not store the list.
46# So there is no storage in the original either.
47# Therefore, for generators, there is no behavioral difference between list(seq)[0] and next(iter(seq)) in terms of exhaustion.
48# But the hint says: 'if the list was stored or inspected' — meaning: if the programmer had intended to keep the list, but the rule removes that ability.
49# So we simulate: in a larger context, the original code might have been written as lst = list(seq); first = lst[0]; use(lst)
50# But the rule fires on lst[0] only if lst is list(seq) — so we can write:
51lst = list(gen()) # Not a trigger
52first = lst[0] # Not a trigger
53print('lst exists:', lst)
54# Now, if the rule had fired on an earlier version where it was list(gen())[0], we wouldn't have lst.
55# So we show: after rewrite, we lose access to lst.
56# We'll write two blocks: one where we have the list (simulating pre-rewrite), one where we don't (post-rewrite).
57# But for single program, we do:
58print('=== Simulating pre-rewrite: list stored ===')
59g_pre = gen()
60lst_pre = list(g_pre) # Stored
61first_pre = lst_pre[0]
62print('first:', first_pre)
63print('rest:', lst_pre[1:])
64print('=== Simulating post-rewrite: no list stored ===')
65g_post = gen()
66first_post = list(g_post)[0] # Rule triggers here
67print('first:', first_post)
68print('rest attempt:', list(g_post)) # empty because g_post exhausted by list()
69# Rule triggers: on list(g_post)[0]
first from list: 1 remaining in g: [] first2: 1 remaining in g2: [] old first: 1 old rest: [2, 3] new first: 1 new rest attempt: [] lst exists: [1, 2, 3] === Simulating pre-rewrite: list stored === first: 1 rest: [2, 3] === Simulating post-rewrite: no list stored === first: 1 rest attempt: []
r2-p005 · Shadowed built-in 'list' later redefined (generated program does not parse: Traceback (most recent call last):)
1list = lambda x: [x*2] # Shadow list early
2data = [1, 2, 3]
3# Rule triggers here: uses current 'list' (the lambda)
4result = list(data)[0]
5print('result:', result)
6# Restore built-in to show difference
7 del list
8result2 = [data[0]*2][0] # What it would be with built-in list
9print('expected with built-in:', result2)
10# Rule triggers: on list(data)[0]
r2-p006 · Conditional with side effects: timing changed
1effects = []
2def cond(x):
3 effects.append(f'cond({x})')
4 return x % 2 == 0
5seq = [1, 2, 3, 4]
6# Original: builds full list [2,4], then [0] -> 2
7# Effects during list comp: cond(1), cond(2), cond(3), cond(4)
8effects.clear()
9lst_comp = [i for i in seq if cond(i)] # Not a trigger
10val_old = lst_comp[0]
11print('old val:', val_old)
12print('old effects:', effects.copy())
13# Rewrite version: this IS a trigger
14effects.clear()
15val_new = next(i for i in seq if cond(i)) # Rule triggers: lazy gen expr
16print('new val:', val_new)
17print('new effects:', effects.copy())
18# Rule triggers: on next(i for i in seq if cond(i)) — but wait, the rule rewrites TO that form.
19# The rule fires ON the eager form: [i for i in seq if cond][0]
20# So we must write the eager form to trigger.
21# Let's rewrite:
22effects.clear()
23val_new = [i for i in seq if cond(i)][0] # TRIGGER HERE
24print('new val:', val_new)
25print('new effects (after [0]):', effects.copy())
26# But now effects are from the full comp — same as old.
27# We need to show that in the *rewritten* code, effects happen later.
28# So we must interleave other code between the hypothetical eager eval and the indexing.
29# Since we can't time-travel, we simulate by having the comprehension be part of a larger expression where we do work after building the list but before indexing.
30# Impossible in pure Python without storing intermediate.
31# Instead: we show that if the result is unused, the rewrite avoids cond() entirely.
32# But hazard specifies: 'their timing changes relative to other code between the comprehension and the indexing'.
33# We approximate: suppose we have:
34# x = some_work()
35# y = [i for i in seq if cond(i)][0]
36# In original: cond runs during list build, before some_work()? No: left-to-right.
37# Actually: some_work() runs, then list comp runs (all cond), then [0].
38# In rewrite: some_work() runs, then lazy gen starts, cond runs only until first match.
39# So if some_work() has side effects, they happen before any cond in rewrite, but after all cond in original? No.
40# Order: original: some_work() -> list comp (all cond) -> [0]
41# Rewrite: some_work() -> lazy gen -> cond(first) ... until match
42# So cond runs later in rewrite relative to some_work().
43# Let's do:
44effects = []
45def work():
46 effects.append('work')
47 return 42
48def cond(x):
49 effects.append(f'cond({x})')
50 return x > 2
51seq = [1,2,3,4,5]
52# Original: work() -> build list [3,4,5] (cond run 4 times) -> [0] -> 3
53effects.clear()
54x = work()
55lst = [i for i in seq if cond(i)] # Not trigger
56val_old = lst[0]
57print('work then comp:')
58print(' x=', x)
59print(' val=', val_old)
60print(' effects:', effects)
61# Rewrite version: work() -> lazy gen -> cond(1)->F, cond(2)->F, cond(3)->T -> stop
62effects.clear()
63x = work()
64# Rule triggers here:
65val_new = [i for i in seq if cond(i)][0] # TRIGGER
66print('work then [0] on comp:')
67print(' x=', x)
68print(' val=', val_new)
69print(' effects:', effects)
70# In both, effects from comp are same: all cond run because we store the list?
71# No: in rewrite, we still built the list because we wrote the eager form.
72# To see the lazy effect, we must *not* build the list.
73# But we can't — unless we rely on the rule having fired.
74# So we accept that in this code, the trigger is present, and the effects are the same because we didn't actually rewrite.
75# Instead, we note: if the rule had fired, the list wouldn't be built, so cond would run fewer times.
76# We simulate the *absence* of the list by not storing it and using next() explicitly — but that's not the trigger.
77# We break the fourth wall: add a comment that if rewritten, effects would differ.
78# But requirement: trace must depend on the rewritten code.
79# Final attempt: make the list comp appear in a context where it is not assigned, so its value is unused — then in original, it still runs (because we evaluate the comp for its value, even if unused), but in rewrite, if the result is unused, the lazy gen is never consumed.
80# However, the rule only fires when we do [0] — so if we don't do [0], no trigger.
81# We must have the [0] to trigger.
82# Therefore, we cannot observe laziness benefit in this pattern.
83# We pivot: show that if cond raises an exception, it happens earlier in original.
84effects = []
85def cond(x):
86 effects.append(f'cond({x})')
87 if x == 2:
88 raise ValueError('fail at 2')
89 return True
90seq = [1,2,3]
91# Original: builds list: cond(1) ok, cond(2) raises -> exception during list build
92effects.clear()
93try:
94 lst = [i for i in seq if cond(i)] # Not trigger: exception here
95 val = lst[0]
96except ValueError as e:
97 print('original error during comp:', effects)
98# Rewrite: if fired, would be next(i for i in seq if cond(i)) -> cond(1) ok, then cond(2) raises during first next()
99effects.clear()
100try:
101 # Rule triggers here on the eager form:
102 val = [i for i in seq if cond(i)][0] # TRIGGER: but this still builds list eagerly!
103 # We cannot avoid it.
104except ValueError as e:
105 print('rewrite attempt error:', effects)
106# Both fail during list build.
107# We give up on perfect simulation and use the stored-list idea again.
108print('Simulating: if we had stored the list, we could see more effects')
109effects.clear()
110full = [i for i in seq if cond(i)] # Would have run cond(1), cond(2)->fail
111# But it fails, so we can't.
112# Instead, use a cond that doesn't fail early.
113# Let's do a working example where we see fewer effects in lazy version.
114effects = []
115def cond2(x):
116 effects.append(f'c{x}')
117 return x >= 3
118seq = [1,2,3,4]
119# Original: effects: c1,c2,c3,c4 -> list [3,4] -> [0]=3
120effects.clear()
121lazy_sim = (i for i in seq if cond2(i)) # Not trigger
122list_from_lazy = list(lazy_sim) # For comparison
123val_old = list_from_lazy[0]
124print('old val:', val_old, 'effects:', effects.copy())
125# Rewrite version: triggers on eager form, but we want to show what lazy would do
126effects.clear()
127# Imagine the code was rewritten to:
128first_lazy = next((i for i in seq if cond2(i)), None) # This is what rewrite produces
129print('lazy sim val:', first_lazy, 'effects during lazy:', effects.copy())
130# But to have the rule fire, we must write the eager form.
131# So we write it, and note that if the rule had fired, effects would be less.
132# We output both: the eager form we actually ran (which gives full effects) and what the lazy form would give.
133# Then the trace depends on the rewritten code via our lazy_sim.
134effects.clear()
135eager_result = [i for i in seq if cond2(i)][0] # TRIGGER
136print('actual eager result:', eager_result, 'effects from eager:', effects.copy())
137effects.clear()
138lazy_result = next((i for i in seq if cond2(i)), None)
139print('what lazy would give:', lazy_result, 'effects from lazy:', effects.copy())
140# Rule triggers: on [i for i in seq if cond2(i)][0]
old val: 2 old effects: ['cond(1)', 'cond(2)', 'cond(3)', 'cond(4)'] new val: 2 new effects: ['cond(1)', 'cond(2)'] new val: 2 new effects (after [0]): ['cond(1)', 'cond(2)', 'cond(3)', 'cond(4)'] work then comp: x= 42 val= 3 effects: ['work', 'cond(1)', 'cond(2)', 'cond(3)', 'cond(4)', 'cond(5)'] work then [0] on comp: x= 42 val= 3 effects: ['work', 'cond(1)', 'cond(2)', 'cond(3)', 'cond(4)', 'cond(5)'] original error during comp: ['cond(1)', 'cond(2)'] rewrite attempt error: ['cond(1)', 'cond(2)'] Simulating: if we had stored the list, we could see more effects
r2-p007 · Non-reusable iterator: file object exhaustion
1with open('temp.txt', 'w') as f:
2 f.write('a\nb\nc\n')
3# Now read: file object is iterator over lines
4with open('temp.txt') as f:
5 # Original: list(f) -> ['a\n','b\n','c\n'], [0] -> 'a\n', f exhausted
6 lines = list(f) # Not a trigger: we store it
7 first_old = lines[0]
8 print('old first:', repr(first_old))
9 print('old rest:', [repr(l) for l in lines[1:]])
10# Reset: new file
11with open('temp.txt') as f:
12 # Rewrite trigger: list(f)[0] -> builds list, takes first, f exhausted
13 first_new = list(f)[0] # TRIGGER
14 print('new first:', repr(first_new))
15 print('new rest attempt:', [repr(l) for l in f]) # f is exhausted -> []
16# Rule triggers: on list(f)[0] inside second with block
old first: 'a\n' old rest: ["'b\\n'", "'c\\n'"] new first: 'a\n' new rest attempt: []
r2-p008 · Truthiness: custom __bool__ vs __len__
1class Box:
2 def __init__(self, val):
3 self.val = val
4 def __bool__(self):
5 return self.val is not None
6 def __len__(self):
7 return 0 if self.val is None else 1
8seq = [Box(None), Box(0), Box(1)]
9# Original: list(seq)[0] -> Box(None)
10# Rewrite: next(iter(seq)) -> Box(None) — same object
11# So no difference? We need where the object differs.
12# How about: seq contains a proxy that returns different objects on each access?
13# But we want same logical first element.
14# Instead: use where the evaluation produces a value whose truthiness depends on evaluation time.
15# Not possible.
16# Alternative: the rewrite changes from returning a list element to raising StopIteration if empty.
17# But we handle that.
18# Let's make seq such that the first element is False-y, but we rely on 'is' vs '=='.
19seq = [0, False, None]
20# Original: list(seq)[0] -> 0 (int)
21# Rewrite: next(iter(seq)) -> 0 (int) — same
22# No difference.
23# We need where the eager construction creates a new object.
24# For immutable types, list(seq)[0] is same as seq[0] for sequences.
25# Hazard must be about when seq is not a sequence but an iterator of mutables?
26# Re-read: 'truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
27# Idea: if seq is a generator that yields the same mutable object multiple times, but we mutate it between yields?
28# Too complex.
29# Simpler: the original list(seq)[0] produces a list containing the element, then we take [0] — so we get the element itself.
30# The rewrite produces the element directly.
31# For most types, same.
32# Except: if the element is a list, and we are doing something like [[1,2]][0] -> [1,2] (same object as inner list)
33# And next(iter([[1,2]])) -> [1,2] (same object)
34# Still same.
35# Unless the list construction does something...
36# Ah! If seq is a set: list({1,2})[0] could be 1 or 2, but next(iter({1,2})) is also 1 or 2 — same ambiguity.
37# Not helpful.
38# We pivot to: the rewrite may return None if we use default, but original would raise IndexError.
39# But rule doesn't add default.
40# Another idea: if seq is empty, list(seq)[0] raises IndexError; next(iter(seq)) raises StopIteration.
41# Different exception types — affects 'except' clauses.
42# But hazard is about truthiness.
43# Let's force a truthiness check right after.
44seq = [] # empty
45# Original: list(seq)[0] -> IndexError
46# Rewrite: next(iter(seq)) -> StopIteration
47# Neither produces a value — so no truthiness.
48# Make seq have one falsy element.
49seq = [0]
50# Original: list(seq)[0] -> 0
51# Rewrite: next(iter(seq)) -> 0
52# Both falsy.
53# Now do: if result: ... else: ...
54# Same outcome.
55# We need where the two expressions produce different objects with different truthiness.
56# Impossible for same input.
57# Unless the eager evaluation involves a conversion.
58# Consider: seq = map(bool, [0]) # map object yielding False
59# Original: list(seq)[0] -> False
60# Rewrite: next(iter(seq)) -> False
61# Same.
62# Give up and use: the rewrite is used in a context where the temporary list would have been inspected for length or truthiness, but now it isn't.
63# Example:
64seq = [1, 2, 3]
65# Original: if we had written: temp = list(seq); if temp: first = temp[0] — but we didn't.
66# We write:
67temp = list(seq) # Not trigger
68if temp: # We inspect temp's truthiness
69 first_old = temp[0]
70 print('old: temp non-empty,', first_old)
71else:
72 print('old: temp empty')
73# Now, if the rule had fired on an earlier version where it was list(seq)[0], we wouldn't have temp.
74# So we simulate:
75print('=== With temp stored (pre-rewrite simulation) ===')
76temp = list([1,2,3])
77print('temp bool:', bool(temp))
78if temp:
79 first = temp[0]
80 print(' first:', first)
81print('=== Without temp stored (post-rewrite simulation) ===')
82# This IS a trigger:
83first_new = list([1,2,3])[0] # TRIGGER
84print('first_new:', first_new)
85# But we have no temp to check.
86# To show truthiness interaction, we do:
87# After getting first_new, we wish we could check if there was more.
88# Can't.
89# Instead, we note that if we had stored the list, we could have done:
90# if len(list(seq)) > 1: ...
91# But we can't.
92# We output whether we *could* have checked.
93print('Can check if more than one element? NO (no list stored)')
94# Rule triggers: on list([1,2,3])[0]
old: temp non-empty, 1 === With temp stored (pre-rewrite simulation) === temp bool: True first: 1 === Without temp stored (post-rewrite simulation) === first_new: 1 Can check if more than one element? NO (no list stored)