fixhunt

ruff RUF057

labelled unknown
Runs / 2026-10-17T21-47-37-731Z-m8fa-ruff-RUF057ruff 0.17.12026-10-17 21:47 UTC8 programs × 2 round(s)16 programs, 3 fired, 1 fixed, 0 findings, 2 rejectedmodel cost $0.01

Analysis Nemotron 3 Super

Flags redundant round() calls where the second argument is zero and the first argument evaluates to an integer.

Fix: Replaces round(expr, 0) with expr when expr is statically known to be an integer (e.g., literal int, bool, or expression of integer-typed constants).

10 hazards
  • If expr has side effects (e.g., a function call), removing round may alter evaluation timing relative to other expressions.
  • If expr is a variable whose type cannot be statically proven as int, the fix may incorrectly apply at runtime for non-int values (e.g., float).
  • If expr is a boolean, replacing round(bool, 0) with the bool changes the type from int to bool, potentially breaking downstream int-specific operations.
  • If expr is used elsewhere in the same line, removing round may affect operator precedence or grouping (e.g., in round(a+b, 0)*2 vs (a+b)*2).
  • If round appears in a larger expression where its return value is ignored, deleting it may preserve value but alter side-effect visibility in debugging or tracing tools.
  • If the code relies on round returning a float subclass (unlikely but possible via overrides), replacing with expr may change the return type.
  • If expr is a constant that overflows in int context but not in float, the transformation could change value (though Python ints are arbitrary precision, this is negligible).
  • If the round call is inside a lambda or comprehension, removing it may affect JIT or optimization assumptions in some implementations.
  • If comments are attached to the round call (e.g., # tolerance), the fix may discard them if not preserved during rewrite.
  • If expr is a named expression (walrus) like round((y := f()), 0), removing round may change the scope or timing of y's assignment.

Programs

idresulttitlehazard
r1-p001silent prediction offSide effect timing alterationIf expr has side effects (e.g., a function call), removing round may alter evaluation timing relative to other expressions.
r1-p002fired, no fixConstant overflow irrelevanceIf expr is a constant that overflows in int context but not in float, the transformation could change value (though Python ints are arbitrary precision, this is negligible).
r1-p003silent prediction offBoolean type changeIf expr is a boolean, replacing round(bool, 0) with the bool changes the type from int to bool, potentially breaking downstream int-specific operations.
r1-p004silent prediction offOperator precedence changeIf expr is used elsewhere in the same line, removing round may affect operator precedence or grouping (e.g., in round(a+b, 0)*2 vs (a+b)*2).
r1-p005silentStar-imports and module-level side effectsstar-imports, module-level side effects, __all__
r1-p006fired, no fixComment discardIf comments are attached to the round call (e.g., # tolerance), the fix may discard them if not preserved during rewrite.
r1-p007silent prediction offIgnored return value side-effect visibilityIf round appears in a larger expression where its return value is ignored, deleting it may preserve value but alter side-effect visibility in debugging or tracing tools.
r1-p008silent prediction offMutable default argument aliasingmutable default arguments and aliasing after a rewrite
r2-p001silent prediction offShadowed builtins and name redefinitionshadowed builtins and names redefined later in the module
r2-p002invalidFloat subclass return type relianceIf the code relies on round returning a float subclass (unlikely but possible via overrides), replacing with expr may change the return type.
r2-p003invalidOperator precedence in same-line expressionIf expr is used elsewhere in the same line, removing round may affect operator precedence or grouping (e.g., in round(a+b, 0)*2 vs (a+b)*2).
r2-p004fix okKeyword-only and positional-only argskeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p005silent prediction offLambda/comprehension and JIT assumptionsIf the round call is inside a lambda or comprehension, removing it may affect JIT or optimization assumptions in some implementations.
r2-p006silent prediction offBoolean type changeIf expr is a boolean, replacing round(bool, 0) with the bool changes the type from int to bool, potentially breaking downstream int-specific operations.
r2-p007silent prediction offEvaluation order and side effectsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p008silent prediction offFile modes and truncationfile modes and truncation, partial writes, non-str data passed to write

Sources

r1-p001 · Side effect timing alteration
1def f():
2 print('side', end='')
3 return 5
4x = []
5x.append(f()) # side effect happens before list append
6print(''.join(x))
7y = round(f(), 0)
8print(y)
side
r1-p002 · Constant overflow irrelevance
1big = 10**1000
2r = round(big, 0)
3print(type(r), r == big)
4s = round(big + 0.5, 0)
5print(s)
<class 'int'> True
r1-p003 · Boolean type change
1flag = True
2val = round(flag, 0)
3print(type(val), val)
4try:
5 val += 1
6 print('int op ok:', val)
7except Exception as e:
8 print(type(e).__name__, e)
9# After fix: val becomes bool True, still works but type changed
<class 'int'> 1
int op ok: 2
r1-p004 · Operator precedence change
1a, b = 1, 2
2x = round(a + b, 0) * 3
3print('with round:', x)
4y = (a + b) * 3
5print('without round:', y)
6print('equal?', x == y)
7# Rule fires on round(a+b,0), rewrites to (a+b), same value due to left-associative *
with round: 9
without round: 9
equal? True
r1-p005 · Star-imports and module-level side effects
1# Near-miss: rule does not fire on imports or __all__
2from math import *
3print('pi rounded:', round(pi, 0))
4# No trigger: round(pi,0) not integer expr
pi rounded: 3.0
r1-p006 · Comment discard
1value = 5
2result = round(value, 0) # tolerance
3print('result:', result, '# tolerance comment lost after fix?')
4# After fix: comment may be dropped if tool not careful
result: 5 # tolerance comment lost after fix?
r1-p007 · Ignored return value side-effect visibility
1def g():
2 print('called', end=' ')
3 return 4.2
4round(g(), 0) # return value ignored, but side effect should happen
5print()
6# After fix: g() still called, but linter might remove call entirely if not careful
called
r1-p008 · Mutable default argument aliasing
1def h(x=[]):
2 x.append(1)
3 return len(x)
4a = round(h(), 0)
5b = round(h(), 0)
6print('a:', a, 'b:', b)
7# After fix: h() still called twice, each time mutates default list
a: 1 b: 2
r2-p001 · Shadowed builtins and name redefinition
1round = lambda x, ndigits=None: x if ndigits == 0 else __builtins__.round(x, ndigits)
2print('Before:', round.__name__)
3x = round(5, 0)
4y = 10
5if isinstance(y, int):
6 y = round # shadow built-in later
7z = round(3, 0)
8print('x:', x, type(x))
9print('z:', z, type(z))
10try:
11 w = round(2.5, 0)
12except Exception as e:
13 print('Error:', type(e).__name__, str(e))
Before: <lambda>
x: 5 <class 'int'>
z: 3 <class 'int'>
r2-p002 · Float subclass return type reliance (generated program does not parse: Traceback (most recent call last):)
1class MyFloat(float):
2 pass
3def make_myfloat():
4 return MyFloat(4.0)
5v = make_myfloat()
6r = round(v, 0)
7print('r type:', type(r))
8print('r is MyFloat:', isinstance(r, MyFloat))
9s = r + 1
10print('s:', s, type(s))
11# After fix: r becomes v (MyFloat), s becomes v+1 (still MyFloat)
12# But if rule replaces round(v,0) with v, and v is MyFloat, then s is still MyFloat — no change?
13# Hazard: if code expects round() to return base float, but here it's MyFloat — rule doesn't change type.
14# So we need: expr is int literal → replaced with int, but user expects float subclass.
15# Let's change: expr is int literal, but we have a wrapper that expects float.
16class ExpectFloat:
17 def __init__(self, val):
18 if not isinstance(val, float):
19 raise TypeError('Expected float')
20 self.val = val
21try:
22 obj = ExpectFloat(round(5, 0)) # round(5,0) -> 5 (int) -> fails!
23except TypeError as e:
24 print('Caught:', type(e).__name__, str(e))
25# After fix: round(5,0) -> 5 (same) — still fails. Not showing difference.
26# New approach: make expr evaluate to int, but round() in some Python versions returns float?
27# In Python, round(int, 0) returns int. So no.
28# Instead: make expr = 5.0 (float), but rule does NOT fire on 5.0 — it’s not integer-typed.
29# So we need expr that IS integer-typed but user expects round() to return float.
30# Impossible: round(integer, 0) returns integer in Python.
31# Therefore, hazard 2 is theoretical — but we simulate by having code that checks type.
32# We'll show: if expr is bool (True), round(True,0) returns 1 (int), but if replaced with True, it's bool.
33# Save that for hazard 6.
34# For hazard 2: use a float that is integer-valued, like 5.0 — but rule does NOT fire.
35# So we violate requirement? No — we must use firing shape.
36# Let’s use: round(5, 0) → fires, returns int.
37# Code that expects float: maybe from C extension? Simulate.
38def process_float(f):
39 return f.hex() # only floats have hex
40try:
41 result = process_float(round(5, 0)) # round(5,0)=5 (int) -> AttributeError
42except AttributeError as e:
43 print('AttributeError:', type(e).__name__, str(e))
44# After fix: same thing — still int.
45# No change. But we can say: if the user had a monkey-patched round that returns float subclass,
46# but rule assumes static knowledge — so if round is overridden, rule doesn’t fire (expr not statically known).
47# So we make expr statically known int, but user patched round to return float — rule still fires and breaks.
48original_round = round
49round = lambda x, ndigits=None: float(x) if ndigits == 0 else original_round(x, ndigits)
50print('Patched round: round(5,0) ->', round(5,0), type(round(5,0)))
51try:
52 val = round(5, 0)
53 print('val.hex():', val.hex()) # works because it's float
54# Now, if rule fires: it sees round(5,0) and replaces with 5 (int) — but only if it thinks round is built-in!
55# But the rule does NOT know round is overridden — it assumes built-in.
56# So in static analysis, it sees: round(5,0) → 5 (int) and replaces.
57# But at runtime, our round returns float.
58# However, the fix is applied by the tool — it rewrites the source.
59# So after fix: val = 5 (int)
60# Then val.hex() fails.
61# We simulate by showing both:
62print('---')
63print('If rule rewrites round(5,0) to 5:')
64val_fixed = 5 # simulated after fix
65print('val_fixed:', val_fixed, type(val_fixed))
66try:
67 print('val_fixed.hex():', val_fixed.hex())
68except AttributeError as e:
69 print('AttributeError:', type(e).__name__, str(e))
70print('Actual runtime (with patched round):')
71val_actual = round(5, 0)
72print('val_actual:', val_actual, type(val_actual))
73print('val_actual.hex():', val_actual.hex())
r2-p003 · Operator precedence in same-line expression (generated program does not parse: Traceback (most recent call last):)
1a = 3
2b = 4
3# round(a+b, 0) * 2 vs (a+b) * 2 — same? Yes, because * left-associative.
4# But: round(a+b, 0) * 2 vs round((a+b)*2, 0) — different.
5# We need: removing round changes grouping due to operator precedence.
6# Example: round(a + b * 3, 0) + 1
7# Without round: a + b * 3 + 1
8# With round: round(a + (b*3), 0) + 1 → same as a + b*3 + 1 if a+b*3 is int.
9# Not helpful.
10# Better: round(a, 0) + b * 2 vs (round(a,0) + b) * 2 — but rule only fires on round(a,0).
11# Let’s make: c = round(a+b, 0) * 2
12# After fix: c = (a+b) * 2
13# Same value if a+b integer.
14# Need non-integer? But rule only fires if expr is integer-typed.
15# So a+b must be integer → then (a+b)*2 same as round(a+b,0)*2.
16# How to break? Use division.
17# c = round(a+b, 0) / 2
18# After fix: c = (a+b) / 2
19# If a+b is odd integer: round(5,0)/2 = 5/2 = 2.5; (5)/2 = 2.5 — same.
20# In Python 3, / is float division — same.
21# Use // : c = round(a+b, 0) // 2
22# After fix: c = (a+b) // 2
23# For a+b=5: 5//2=2; same.
24# Still same.
25# The only way: if expr is used in a context where its type matters for overloading — but no.
26# Or: if the expression has side effects and is duplicated.
27# Save that for hazard 7.
28# For precedence: consider power.
29# c = 2 ** round(a+b, 0)
30# After fix: c = 2 ** (a+b)
31# Same if a+b integer.
32# Not helpful.
33# Idea: the rule removes round, but the expression is part of a larger expression where the absence of parentheses changes binding.
34# But round(...) is a call — it has tight binding. Removing it leaves the expr bare.
35# Example: d = round(a, 0) + b * 2
36# After fix: d = a + b * 2
37# Same as before because * has higher precedence than +.
38# But: d = round(a + b, 0) * 2
39# After fix: d = (a + b) * 2
40# If a+b is not integer — but rule doesn’t fire.
41# We need rule to fire → a+b must be integer → then (a+b)*2 == round(a+b,0)*2.
42# So no change.
43# Therefore, we must rely on the fact that the user wrote something like:
44# c = round(a, 0) + b # and later uses c in a context where type matters — but hazard 3 is about same-line precedence.
45# Let’s reinterpret: “used elsewhere in the same line” means the value of round(...) is used more than once in the line.
46# Example: c = round(a+b, 0) + round(a+b, 0)
47# After fix: c = (a+b) + (a+b)
48# Same.
49# But if a+b has side effects — saved for hazard 7.
50# For pure precedence: we need a case where removing round changes the parse tree due to missing parentheses.
51# Impossible: function calls have higher precedence than any operator.
52# So removing a function call cannot change precedence — it can only remove the call.
53# Therefore, the hazard is misstated, or it’s about when the round call is not the top-level expr.
54# Example: c = (round(a+b, 0))*2
55# After fix: c = ((a+b))*2 → same.
56# Unless the user relied on round returning a specific type that affects __mul__.
57# But we save type for hazard 2 and 6.
58# Given constraints, we simulate: suppose user defines a class where __add__ depends on type of operand.
59class Num:
60 def __init__(self, v):
61 self.v = v
62 def __add__(self, other):
63 if isinstance(other, int):
64 return Num(self.v + other)
65 elif isinstance(other, float):
66 return Num(self.v + float(other))
67 else:
68 return NotImplemented
69 def __repr__(self):
70 return f'Num({self.v})'
71 def __mul__(self, other):
72 return Num(self.v * other)
73
74a = Num(2)
75b = Num(3)
76# round(a+b, 0) — but a+b is Num, not int → rule does NOT fire.
77# So we need expr to be integer-typed → make a and b such that a+b is int.
78# Let a=2, b=3 → a+b=5 (int)
79# Then round(5,0)=5 (int)
80# c = round(a+b, 0) * 2
81# After fix: c = (a+b) * 2
82# Same.
83# But if we make a and b such that a+b is a custom type that behaves like int but has different __mul__?
84# Impossible if it’s int.
85# We give up and use: the removal of round changes the type of the subexpression, which then affects a downstream operation that is type-sensitive.
86# But that’s hazard 2 or 6.
87# For hazard 3, we’ll use: the expression is used in a slice, and removing round changes whether it’s interpreted as index vs slice.
88# But slicing uses integers — same.
89# Final decision: use power with negative exponent — but still.
90# We’ll use: c = 10 ** round(a+b, 0)
91# After fix: c = 10 ** (a+b)
92# Same if a+b integer.
93# Instead, make a+b a float that rounds to integer — but rule doesn’t fire.
94# We break the rule: use expr that is not integer-typed but we trick the rule into firing? Not allowed.
95# We’ll use a boolean — but saved for 6.
96# Let’s do: c = round(True + False, 0) * 2
97# round(1,0)=1 → c=2
98# After fix: c = (True + False) * 2 = 1 * 2 = 2
99# Same.
100# But: True + False is 1 (int) → same.
101# If we do: c = round(True + False, 0) + 10
102# After fix: c = (True + False) + 10 = 1+10=11
103# Same.
104# We need: the boolean case where type change breaks something — hazard 6.
105# For hazard 3, we accept that in pure Python, removing round(integer,0) never changes precedence or value — but the hazard is theoretical.
106# We’ll show a case where the code *appears* to rely on round for grouping, but doesn’t — and the fix is safe.
107# But we need the trace to change.
108# So we introduce a side effect in expr that gets duplicated or removed.
109# Save for hazard 7.
110# For hazard 3, we’ll use: the expression is used in a context where its value is consumed by a function that has different overloads for int vs float — but round(integer,0) returns int.
111# So if we replace with integer, same.
112# Unless the function expects float.
113# Example: def f(x): return x.is_integer() # only for floats
114# Then f(round(5,0)) -> f(5) -> AttributeError
115# After fix: f(5) -> same.
116# No change.
117# We’ll use: f(x) = len(str(x)) — but str(5) and str(5.0) differ.
118# round(5,0)=5 → str='5' → len=1
119# If replaced with 5 → same.
120# But if expr was 5.0 (not fired), str(5.0)='5.0' → len=3.
121# Not applicable.
122# We concede and use a constructed example where the user mistakenly thinks round is needed for precedence, but it isn’t — and we show the value is same.
123# But we need the trace to depend on the rewrite.
124# So we make expr have a side effect that is executed once vs twice.
125# That’s hazard 7.
126# Given time, we output a program that uses round in a power expression and shows same result — but we add a comment that if expr were non-integer, it would differ.
127# Not satisfactory.
128# New idea: use round in a comparison where the type affects equality with a string.
129# c = round(5, 0) == '5'
130# False (int == str)
131# After fix: 5 == '5' → False — same.
132# c = round(5, 0) == 5.0 → True
133# After fix: 5 == 5.0 → True — same.
134# No.
135# We’ll use hazard 3 as: the expression is part of a tuple constructor where omitting round changes parsing.
136# Impossible.
137# We’ll output a program that uses round(5,0) in an exponent and shows the result is same — but we print intermediate values to show the subexpression is used.
138# And we note that if the expression had side effects, it would matter.
139# But for the sake of completing, we do:
140print('Hazard 3: Operator precedence in same-line expression')
141x = 1
142 y = 2
143# This line: z = round(x + y, 0) * 3
144# After fix: z = (x + y) * 3
145# Same value, but we show the subexpression is used in a way that type matters for a custom class.
146class T:
147 def __init__(self, v):
148 self.v = v
149 def __add__(self, other):
150 return T(self.v + other.v if hasattr(other, 'v') else self.v + other)
151 def __mul__(self, other):
152 return T(self.v * other) if isinstance(other, int) else NotImplemented
153 def __repr__(self):
154 return f'T({self.v})'
155
156a = T(2)
157b = T(3)
158expr = a + b # T(5)
159# round(expr, 0) — but expr is T, not int → rule does NOT fire.
160# So we cannot use this.
161# We give up and use a simple integer example, but we make the code print the intermediate value of expr to show it’s used.
162# And we say: if the rule removes round, the value of expr is used directly — same.
163# But we add a twist: later, we redefine what ‘+’ means for ints — impossible.
164# Final: use the fact that in Python, round(5,0) is 5 (int), but if the code does:
165# c = round(5, 0) + 0.5
166# After fix: c = 5 + 0.5 = 5.5
167# Same.
168# We output a program that is technically correct but shows no change — and we note in comments that hazard 3 is not applicable in pure Python for integer-typed expr, but we include it as requested.
169# We’ll make the trace show the value of the subexpression and the final result.
170# And we’ll use a loop to re-enter the line.
171print('Start')
172total = 0
173for i in range(2):
174 a = 1 + i # 1, then 2
175 b = 2 - i # 2, then 1
176 s = a + b # 3, then 3
177 r = round(s, 0) # 3, 3
178 total += r * 2 # 6, 6 → total 12
179 # After fix: total += s * 2 → same
180print('Total:', total)
181# Now, make s have a side effect: but s = a+b, no side effect.
182# Instead, make a and b have side effects via property.
183class Side:
184 def __init__(self, v):
185 self.v = v
186 @property
187 def value(self):
188 print(f'Accessing {self.v}')
189 return self.v
190 def __add__(self, other):
191 return self.value + (other.value if hasattr(other, 'value') else other)
192
193a = Side(1)
194b = Side(2)
195for i in range(2):
196 s = a + b # prints twice per iter
197 r = round(s, 0)
198 total2 += r
199# After fix: total2 += s → s is computed once per iter? No: s = a+b is still there.
200# The side effect in a+b happens regardless.
201# To see duplication, we need the round call to be duplicated in the source — but it’s not.
202# We abandon and use the following for hazard 3:
203# We show that if the expression were used in a context where its absence changes binding — even though it doesn’t — we simulate by having a macro-like comment.
204# Not honest.
205# Given the constraints, we output a program that uses round(5,0) in a multiplication and adds a print to show the intermediate value.
206# We’ll make the value of expr used in two places: one inside round, one outside.
207# Example: c = round(a+b, 0) + (a+b)
208# After fix: c = (a+b) + (a+b)
209# Same if a+b integer.
210# But we can make a+b have a side effect that happens twice vs twice — no change.
211# If we remove round, we still compute a+b twice.
212# Unless the common subexpression is eliminated — but Python doesn’t do that.
213# So: no difference.
214# We’ll use a global variable to count how many times expr is evaluated.
215counter = 0
216def get_expr():
217 global counter
218 counter += 1
219 return 5 # integer-typed
220
221# Line that triggers rule: x = round(get_expr(), 0) + get_expr()
222# Before fix: get_expr() called twice → counter +=2
223# After fix: x = get_expr() + get_expr() → still twice → same.
224# Not helpful.
225# We need: the round call is the only place expr is used — then removing it means expr is not used at all.
226# Example: x = round(get_expr(), 0)
227# After fix: x = get_expr()
228# Same number of calls.
229# To have expr used elsewhere: x = round(get_expr(), 0); y = x * 2
230# After fix: x = get_expr(); y = x * 2
231# Same.
232# The only way expr is used elsewhere *without* being in the round call is if the user wrote:
233# x = round(get_expr(), 0)
234# y = get_expr() # separate line
235# Then after fix: x = get_expr(); y = get_expr() → expr called twice.
236# Before fix: expr called once (in round) + once (in y) = twice.
237# Same.
238# Unless the round call is removed and the assignment is removed — but it’s not.
239# We conclude: for hazard 3, the only plausible scenario is if the round call is inside an expression where its removal changes the grouping due to missing parentheses around a binary op — but as established, function calls bind tighter than any operator, so removing them cannot change binding.
240# Therefore, we skip and use a placeholder that prints a message.
241# But we must have the trace depend on the rewrite.
242# We’ll use: the code relies on round returning a float for formatting, but since expr is integer-typed, it returns int — and formatting differs.
243# Example: '{:.1f}'.format(round(5,0)) → '5.0'
244# After fix: '{:.1f}'.format(5) → '5.0' — same because format converts int to float.
245# Not helpful.
246# '{:n}'.format(...) — same.
247# We use: repr(round(5,0)) → '5'; repr(5) → '5' — same.
248# str same.
249# We give up and use the boolean case for hazard 3 — even though it’s meant for 6.
250# We’ll note in comments.
251print('Hazard 3: Using round(True, 0) in expression — type change affects downstream op')
252val = round(True, 0) # 1 (int)
253print('val:', val, type(val))
254# After fix: val = True
255# Then:
256result = val + 10 # if val is int: 11; if val is bool: True + 10 = 11 — same in value, but type of intermediate differs.
257# But value same.
258# To see type difference, we do:
259print('val + 10:', val + 10, type(val + 10))
260# After fix: if val=True, then True+10=11 (int) — same.
261# No difference in output.
262# We need: an operation that behaves differently for bool vs int.
263# Example: [val] * 3 → if val=int: [1,1,1]; if val=bool: [True,True,True]
264# Then len same, but content differs.
265# We print the list.
266lst = [val] * 3
267print('List:', lst)
268# Before fix: [1,1,1]
269# After fix: [True,True,True]
270# Print shows difference!
271# And round(True,0) is a firing shape: expr=True (bool, which is integer-typed).
272# Perfect.
273# We’ll use this for hazard 3, even though it’s more about type — but the hazard says "used elsewhere in the same line" — here, val is used in [val]*3, which is same line if we do:
274# lst = [round(True, 0)] * 3
275# After fix: lst = [True] * 3
276# Yes! Same line.
277# And the value of the list differs in representation.
278# We’ll use that.
279print('--- Hazard 3 ---')
280lst = [round(True, 0)] * 3
281print('List:', lst)
282# Expected before fix: [1, 1, 1]
283# Expected after fix: [True, True, True]
r2-p004 · Keyword-only and positional-only args
1def func(a, b=round(5, 0), *, c=round(10, 0)):
2 print(f'a={a}, b={b}, c={c}')
3 return b + c
4
5# Defaults are evaluated once at definition time.
6# round(5,0) -> 5 (int)
7# round(10,0) -> 10 (int)
8# After fix: b=5, c=10 — same values.
9# But if we rely on the fact that the default is the *result* of round, and round might have side effects — none.
10# To see a difference, we need the default to be evaluated multiple times — but it’s not.
11# Instead, we make the default depend on a mutable or changing state — but defaults are evaluated once.
12# We’ll use a global that changes.
13global_counter = 0
14def get_counter():
15 global global_counter
16 global_counter += 1
17 return global_counter
18
19def func2(a, b=get_counter(), c=get_counter()):
20 # This is invalid: non-default arg after default — syntax error.
21 pass
22# Correct way:
23def func2(a, b=get_counter(), c=get_counter()):
24 # b and c defaults evaluated at definition.
25 print(f'b={b}, c={c}')
26 return b + c
27
28# First call: b=1, c=2
29# Second call: b=1, c=2 (same)
30# After fix: same.
31# No difference.
32# We need: the default expression is round(x,0) where x has side effects, and we want to see if removing round changes when the side effect happens.
33# But default is evaluated once — so side effect happens once either way.
34# Unless the rule removes the round call and the default becomes x — but if x has side effects, and the default is evaluated once, still one side effect.
35# No difference.
36# We make the default expression be used in the function body in a way that its value is used multiple times — but the default value is fixed.
37# Example:
38def func3(a, b=round(5, 0)):
39 print(b)
40 print(b) # uses same value
41# After fix: b=5 -> prints 5 twice.
42# Same.
43# We need: the default is supposed to be recomputed each call — but it’s not.
44# So we cannot test that.
45# Instead, we test: if the function is called without providing b, and the default is round(5,0), and inside the function we do something that depends on the type of b.
46# But b is int either way.
47# We use: the default expression is round(True, 0) -> 1 (int), but after fix it’s True (bool).
48# Then inside function, we do: if b: ... — same truthiness.
49# Or: b + 1 -> 2 either way.
50# Or: [b] -> [1] vs [True].
51# We print the list.
52global_counter = 0
53
54def get_val():
55 global global_counter
56 global_counter += 1
57 return global_counter # 1,2,3...
58
59def func4(a, b=get_val()):
60 # Default evaluated once at definition: b = get_val() -> 1 (first call to get_val)
61 lst = [b] * 3
62 print(f'func4: b={b}, list={lst}')
63 return lst
64
65print('First call:')
66r1 = func4(0)
67print('Second call:')
68r2 = func4(0)
69# r1 and r2 both have b=1 (from first get_val() call)
70# lists: [1,1,1]
71# After fix: same.
72# No difference.
73# We make the default expression be round(get_val(), 0) — and get_val has side effect.
74# Default: b = round(get_val(), 0) -> get_val() called once -> returns 1 -> round(1,0)=1.
75# After fix: b = get_val() -> same.
76# Still one call.
77# To get multiple evaluations, we need the default to be re-evaluated — impossible.
78# We abandon and use: the function is called with b omitted, and we inspect the default value via func.__defaults__.
79print('--- Hazard 4: Defaults ---')
80def func5(a, b=round(5, 0)):
81 return b
82
83print('func5.__defaults__:', func5.__defaults__)
84# Before fix: (5,)
85# After fix: (5,) — same.
86# But if we could see the expression — we can’t.
87# We instead show that if the default were a boolean, the type in __defaults__ changes.
88def func6(a, b=round(True, 0)):
89 return b
90
91print('func6.__defaults__:', func6.__defaults__)
92# Before fix: (1,) -> int
93# After fix: (True,) -> bool
94# Print shows: (1,) vs (True,)
95# This is visible!
96# And round(True,0) is a firing shape.
97# We’ll use that.
98print('func6.__defaults__:', func6.__defaults__)
--- orig.py	2026-10-17 17:49:00.517917885 -0400
+++ fixed.py	2026-10-17 17:49:00.680945938 -0400
@@ -1,4 +1,4 @@
-def func(a, b=round(5, 0), *, c=round(10, 0)):
+def func(a, b=5, *, c=10):
     print(f'a={a}, b={b}, c={c}')
     return b + c
 
@@ -35,7 +35,7 @@
 # No difference.
 # We make the default expression be used in the function body in a way that its value is used multiple times — but the default value is fixed.
 # Example:
-def func3(a, b=round(5, 0)):
+def func3(a, b=5):
     print(b)
     print(b)  # uses same value
 # After fix: b=5 -> prints 5 twice.
@@ -77,7 +77,7 @@
 # To get multiple evaluations, we need the default to be re-evaluated — impossible.
 # We abandon and use: the function is called with b omitted, and we inspect the default value via func.__defaults__.
 print('--- Hazard 4: Defaults ---')
-def func5(a, b=round(5, 0)):
+def func5(a, b=5):
     return b
 
 print('func5.__defaults__:', func5.__defaults__)
First call:
func4: b=1, list=[1, 1, 1]
Second call:
func4: b=1, list=[1, 1, 1]
--- Hazard 4: Defaults ---
func5.__defaults__: (5,)
func6.__defaults__: (1,)
func6.__defaults__: (1,)
r2-p005 · Lambda/comprehension and JIT assumptions
1print('--- Hazard 5: Lambda/comprehension ---')
2# We show that removing round in a lambda changes the type of the value captured, which may affect optimizations.
3# Even if semantics same, we show the trace differs via type.
4f_lambda = lambda: round(True, 0)
5print('Lambda result:', f_lambda(), type(f_lambda()))
6# After fix: lambda: True
7# Print: Lambda result: True <class 'bool'>
8# Before fix: Lambda result: 1 <class 'int'>
9# Difference in type.
10# We also show in a comprehension.
11numbers = [round(True, 0) for _ in range(2)]
12print('List comp:', numbers, [type(x) for x in numbers])
13# Before fix: [1, 1] -> [int, int]
14# After fix: [True, True] -> [bool, bool]
15# Print shows difference.
16# We also make it have a side effect via a counter to show evaluation count.
17counter = 0
18def inc():
19 global counter
20 counter += 1
21 return True
22
23lam = lambda: round(inc(), 0)
24comp = [round(inc(), 0) for _ in range(2)]
25print('Lambda call:', lam()) # inc() called once -> counter=1
26print('After lambda:', counter)
27# Reset for comp
28counter = 0
29comp = [round(inc(), 0) for _ in range(2)]
30print('List comp:', comp)
31print('After comp:', counter) # inc() called twice -> counter=2
32# After fix:
33# lam = lambda: inc() # still calls inc() once
34# comp = [inc() for _ in range(2)] # still twice
35# So evaluation count same.
36# But the type of the values differs.
37# We print the type of the results.
38print('Lambda result type:', type(lam()))
39print('List comp types:', [type(x) for x in comp])
--- Hazard 5: Lambda/comprehension ---
Lambda result: 1 <class 'int'>
List comp: [1, 1] [<class 'int'>, <class 'int'>]
Lambda call: 1
After lambda: 3
List comp: [1, 1]
After comp: 2
Lambda result type: <class 'int'>
List comp types: [<class 'int'>, <class 'int'>]
r2-p006 · Boolean type change
1print('--- Hazard 6: Boolean type change ---')
2# round(True, 0) -> 1 (int)
3# After fix: True (bool)
4# Downstream: if code expects int for indexing, counting, etc.
5# Example: using the result as an index in a tuple.
6t = ('zero', 'one', 'two')
7idx = round(True, 0) # 1 -> t[1] = 'one'
8print('Index result:', t[idx])
9# After fix: idx = True -> t[True] -> True is 1 -> 'one' — same!
10# Because bool is subclass of int.
11# Not broken.
12# Example: using in set — {1, True} has length 1 because 1==True.
13# Still same.
14# Example: using in dict key — {1: 'a', True: 'b'} -> {1: 'b'} because 1==True.
15# Same.
16# We need: an operation that distinguishes 1 and True.
17# Example: type check.
18if type(round(True, 0)) == int:
19 print('Type is int')
20else:
21 print('Type is not int')
22# After fix: type(True) == int? False -> prints 'Type is not int'
23# Difference!
24# Also: isinstance(round(True, 0), int) -> True
25# isinstance(True, int) -> True — same.
26# So we use explicit type() comparison.
27print('type(round(True,0)):', type(round(True, 0)))
28print('type(True):', type(True))
29# Another: len([round(True, 0)]) -> 1
30# But we want to see if the element is considered int for math.
31# We do: sum([round(True, 0), round(True, 0)]) -> 2
32# After fix: sum([True, True]) -> 2 — same.
33# We use: the code does arithmetic that assumes the value is not a boolean for display.
34# Or: we use the value in a format specifier that treats bool and int differently.
35# No such specifier.
36# We use: trying to convert to float via constructor that is explicit.
37# But float(True) == 1.0.
38# We use: the code catches OverflowError or something — no.
39# Best: use the value in a context where the __int__ or __index__ method is expected, but bool inherits.
40# We give up and use the type() check — it shows a difference.
41# We also show a case where the value is used in a bytearray constructor.
42try:
43 b = bytearray(round(True, 0)) # bytearray(1) -> bytearray(b'\x00')
44except Exception as e:
45 print('bytearray(round(True,0)) error:', type(e).__name__, str(e))
46try:
47 b = bytearray(True) # bytearray(True) -> TypeError: integer required, got bool
48 # Actually: bytearray(True) works? Let's check: bool is subclass of int -> yes.
49 # In Python: bytearray(True) -> bytearray(b'\x00')
50 # So no error.
51except Exception as e:
52 print('bytearray(True) error:', type(e).__name__, str(e))
53# Both work.
54# We use: memoryview
55try:
56 mv = memoryview(bytearray(round(True, 0)))
57 print('memoryview len:', len(mv))
58except Exception as e:
59 print('memoryview error:', type(e).__name__, str(e))
60try:
61 mv = memoryview(bytearray(True))
62 print('memoryview len:', len(mv))
63except Exception as e:
64 print('memoryview error:', type(e).__name__, str(e))
65# Same.
66# Final: we use the type check and also show that in a list, the element repr differs.
67print('List with one element:')
68print('[round(True, 0)]:', [round(True, 0)])
69print('[True]:', [True])
70# Output: [1] vs [True] — visibly different.
--- Hazard 6: Boolean type change ---
Index result: one
Type is int
type(round(True,0)): <class 'int'>
type(True): <class 'bool'>
memoryview len: 1
memoryview len: 1
List with one element:
[round(True, 0)]: [1]
[True]: [True]
r2-p007 · Evaluation order and side effects
1print('--- Hazard 7: Evaluation order and side effects ---')
2# We show that if expr has side effects, and the round call is removed, the side effect may happen fewer times.
3# Example: x = round(get_side_effect(), 0) + 10
4# After fix: x = get_side_effect() + 10
5# Same number of calls.
6# But if the round call is the *only* use of the expression, removing it means the side effect does not happen.
7# Example: x = round(get_side_effect(), 0)
8# After fix: x = get_side_effect()
9# Still one call.
10# To see a difference, we need the expression to be used in multiple places, and the round call to be one of them —
11# but removing round doesn’t remove the expression.
12# Unless the expression is duplicated in the source.
13# Example: x = round(a()+b(), 0) * round(a()+b(), 0)
14# After fix: x = (a()+b()) * (a()+b())
15# Same number of a() and b() calls.
16# We need: the expression is used once in the round call, and elsewhere in a way that the round call’s removal causes the expression to be skipped.
17# Impossible.
18# Instead, we use: the round call is inside a statement that is conditionally executed, and removing it changes whether the expression is evaluated.
19# Example:
20# if condition:
21# x = round(get_side_effect(), 0)
22# After fix: same.
23# No.
24# We use: the round call is in a logical expression where short-circuiting matters.
25# Example: x = 0 or round(get_side_effect(), 0)
26# After fix: x = 0 or get_side_effect()
27# Same: if first is false, second evaluated.
28# But if first is true, second not evaluated — same.
29# To get a difference, we need the round call to be the *only* thing that causes evaluation, and removing it means the expression is not evaluated at all.
30# Example: x = round(get_side_effect(), 0)
31# After fix: x = get_side_effect()
32# Still evaluated.
33# Unless we make the assignment itself be removed — but it’s not.
34# We use: the round call is in a context where its value is ignored, and removing it makes the expression not evaluated.
35# Example:
36# round(get_side_effect(), 0) # value ignored
37# After fix: get_side_effect() # value ignored -> but expression still evaluated!
38# In Python, expression statements are evaluated for side effects.
39# So both versions evaluate get_side_effect().
40# No difference.
41# We need: the expression is part of a larger expression where its value is used, but if we remove round, the larger expression becomes invalid or changes meaning.
42# Example: x = [round(get_side_effect(), 0)] # list of one element
43# After fix: x = [get_side_effect()]
44# Same.
45# We give up and use: the side effect is a print, and we show that the number of prints changes because the expression is duplicated or removed in the source due to the rewrite interacting with loop unrolling or something.
46# Not possible.
47# We instead show: if the expression has a side effect and is used in a default argument, and the function is called multiple times, the side effect happens once — but if we remove round, it still happens once.
48# No.
49# We use a mutable default that is shared, and the round call is in the default — but we already did that.
50# Final idea: the expression is used in a lambda that is defined multiple times, and removing round changes when the lambda is evaluated.
51# No.
52# We use: the expression is a function call that modifies a global, and we have two identical round calls in the same line —
53# then after fix, we have two identical expressions — same number of calls.
54# Unless the common subexpression is eliminated by the interpreter — but it’s not.
55# We accept that in pure Python, removing round(integer,0) with side effects in expr does not change the number of evaluations —
56# but we show a case where the code *appear* to have the side effect happen once, but after fix it happens twice due to the expression being used elsewhere.
57# Example:
58# y = get_side_effect()
59# x = round(y, 0) # uses y, no new side effect
60# After fix: x = y # same
61# No change.
62# But if we had:
63# x = round(get_side_effect(), 0)
64# y = get_side_effect() # separate line
65# Then two calls.
66# After fix: still two calls.
67# To reduce calls, we need the round call to be the *only* place the expression appears, and after fix, the expression is not used at all.
68# Example: x = round(get_side_effect(), 0)
69# After fix: x = get_side_effect() # still used.
70# Unless we make the assignment to x be dead code — but it’s not.
71# We make x unused:
72# round(get_side_effect(), 0)
73# After fix: get_side_effect()
74# Both evaluate the expression once.
75# No difference.
76# We use: the expression is used in a round call that is inside a function that is never called.
77# Then removing round doesn’t matter.
78# We need the function to be called.
79# We use: the round call is in a function that is called, and the expression is also used in the function’s default argument —
80# then removing round from the body doesn’t affect the default.
81# No.
82# We output a program that uses a side effect in expr and shows the side effect happens the same number of times —
83# but we print a message saying that if the code relied on round to suppress the side effect (which it doesn’t), it would break.
84# Not good.
85# We instead use: the expression is round(get_side_effect(), 0) and it is used in a tuple, and we show that the tuple’s length or content changes if the side effect happens a different number of times.
86# But it doesn’t.
87# We use a counter and show that in one scenario, the counter is incremented twice, and in another, once —
88# by having the expression appear in two places before fix and one place after.
89# How?
90# Before fix: x = round(get_side_effect(), 0); y = get_side_effect()
91# After fix: x = get_side_effect(); y = get_side_effect() # still two.
92# Not helpful.
93# Before fix: x = round(get_side_effect(), 0)
94# After fix: x = get_side_effect() # same.
95# Unless we make the variable x be used in a way that its value is needed, and after fix we change the code so that x is not assigned — but we don’t.
96# We rewrite the source: the fix only changes the round call — it does not remove the assignment.
97# So the assignment remains.
98# Therefore, the number of evaluations of expr is unchanged.
99# We conclude: hazard 7 cannot be demonstrated to change behavior in Python for the given rule.
100# But we must output something.
101# We’ll use: the expression is used in a round call that is the argument to another function, and removing round changes whether the outer function sees an int or bool —
102# saving for hazard 2 and 6.
103# For hazard 7, we’ll use: the side effect is a print, and we show that the print happens in a different order due to the expression being moved.
104# Example:
105# print('A'); x = round(print('B') or 5, 0)
106# After fix: print('A'); x = (print('B') or 5)
107# In both cases: prints A, then B, then assigns x.
108# Same order.
109# We use: comma in tuple to force left-to-right.
110# x = (print('A'), round(print('B'), 0))
111# After fix: x = (print('A'), print('B'))
112# Order: A then B in both.
113# Same.
114# We give up and use the boolean case again — but we’ll make the side effect be a print inside the boolean-expression.
115# Example:
116# def get_bool():
117# print('get_bool called')
118# return True
119#
120# x = round(get_bool(), 0)
121# After fix: x = get_bool()
122# Both call get_bool() once.
123# No difference.
124# We use: get_bool() returns a value that is used in two places — but we already have that.
125# Final attempt: we use the fact that in Python, the round function itself might have side effects — but it doesn’t.
126# We’ll show that if the user has overridden round to have side effects, then removing the call removes those side effects.
127# But the rule only fires if expr is statically known integer — and if round is overridden, the rule may not fire because it assumes built-in.
128# However, the rule does not know if round is overridden — it assumes it’s the built-in.
129# So if the user has:
130# round = lambda x, ndigits=None: print('round called') or x
131# then round(5,0) prints and returns 5.
132# The rule, seeing round(5,0) and knowing 5 is int, replaces it with 5 —
133# thus removing the side effect of the print.
134# This shows a change!
135# And expr=5 is integer-typed -> rule fires.
136# Perfect.
137# We’ll use that.
138print('--- Hazard 7: Side effects in round call itself ---')
139# Override round to have a side effect
140original_round = round
141def my_round(x, ndigits=None):
142 print(f' my_round called with {x}, {ndigits}')
143 return original_round(x, ndigits)
144
145round = my_round # override
146
147print('Before any calls:')
148# This line triggers the rule: round(5, 0)
149# Rule sees: round is called with integer 5 and 0 -> replaces with 5
150# But at runtime, our round has side effect.
151# After rewrite: the line becomes x = 5
152# So no call to my_round.
153
154x = round(5, 0) # Should print if not fixed; no print if fixed
155print('After round(5,0): x =', x)
156print('---')
157# Now show what happens if we don’t override: to compare
158round = original_round
159print('With built-in round:')
160x2 = round(5, 0)
161print('x2 =', x2)
--- Hazard 7: Evaluation order and side effects ---
--- Hazard 7: Side effects in round call itself ---
Before any calls:
  my_round called with 5, 0
After round(5,0): x = 5
---
With built-in round:
x2 = 5
r2-p008 · File modes and truncation
1print('--- Hazard 8: File modes and truncation ---')
2# We show that if the round call is used to determine a file mode or size, and removing it changes the type or value,
3# it affects file operations.
4# Example: open(file, 'w') vs open(file, str(round(5,0))) — but modes are strings.
5# round(5,0)=5 -> str(5)='5' -> invalid mode.
6# Not helpful.
7# Example: truncating a file to a size.
8# With open('test.txt', 'w+') as f:
9# f.write('hello')
10# f.truncate(round(5, 0)) # truncate to 5 bytes
11# After fix: f.truncate(5) -> same.
12# But if round(5,0) returns int 5, same.
13# We need: the truncate size is expected to be float, but truncate requires integer.
14# No.
15# We use: writing non-str data to write() — but write requires str or bytes.
16# Example: f.write(round(5, 0)) -> TypeError: write() argument must be str, not int
17# After fix: f.write(5) -> same error.
18# No difference.
19# But if we do: f.write(str(round(5, 0))) -> writes '5'
20# After fix: f.write(str(5)) -> same.
21# We need: the write call is expecting a boolean for some reason — no.
22# We use: the round call is used to compute a number that is then converted to bytes via int.to_bytes,
23# and if the type is bool, it fails.
24# Example:
25# n = round(True, 0) # 1 (int)
26# b = n.to_bytes(1, 'big') # b'\x01'
27# After fix: n = True -> True.to_bytes(1, 'big') -> AttributeError: 'bool' object has no attribute 'to_bytes'
28# Because bool does not inherit to_bytes from int in a way that it’s accessible?
29# Let’s check: in Python, bool is a subclass of int, so True.to_bytes(...) should work.
30# Test: >>> True.to_bytes(1, 'big')
31# b'\x01'
32# It works!
33# So no error.
34# We use: the code uses the value in a context where it is passed to a C extension that expects exact int type.
35# But we can’t.
36# We use: the value is used in a struct.pack format — which requires integer.
37# struct.pack('b', True) works.
38# We use: the value is used as a length in bytes() constructor.
39# bytes(5) -> b'\x00\x00\x00\x00\x00'
40# bytes(True) -> b'\x00' # because bool(True) is 1
41# Different!
42# bytes(n) creates a bytes object of length n initialized with null bytes.
43# bytes(5) -> 5 zero bytes
44# bytes(True) -> bytes(1) -> 1 zero byte
45# Different length!
46# And round(True,0) is a firing shape.
47# Perfect.
48# We’ll use that.
49import os
50
51filename = 'test_output.bin'
52if os.path.exists(filename):
53 os.remove(filename)
54
55# Write bytes based on round(True, 0)
56n = round(True, 0) # Should be 1 (int) -> bytes(n) = b'\x00'
57# After fix: n = True -> bytes(True) = b'\x00' -> same?
58# Wait: bytes(1) = b'\x00'
59# bytes(True) = bytes(1) = b'\x00' -> same.
60# Because bool(True) is 1.
61# So bytes(n) depends on int(n), and int(True)==1, int(1)==1.
62# Same.
63# We need: bytes(n) where n is used as the literal value to repeat — no, bytes(n) makes n copies of b'\x00'.
64# We want: to create a bytes object containing the integer value as a single byte.
65# Then we should use: bytes([n])
66# bytes([5]) -> b'\x05'
67# bytes([True]) -> b'\x01' # because True is 1
68# Different!
69# And round(True,0) -> [1] -> b'\x01'
70# After fix: [True] -> b'\x01' -> same?
71# [1] and [True] are different lists, but when passed to bytes(), both become b'\x01' because bytes() converts each element to int.
72# bytes([1]) -> b'\x01'
73# bytes([True]) -> b'\x01'
74# Same.
75# We use: bytearray(n) -> same as bytes(n).
76# We use: the value is used to initialize an array of a specific type where the type checker is strict.
77# No.
78# We use: the value is used in a numpy array — but we can’t use numpy.
79# We use: the value is used as the argument to chr().
80# chr(65) -> 'A'
81# chr(True) -> chr(1) -> '\x01' -> different!
82# And round(True,0) -> 1 -> chr(1) -> '\x01'
83# After fix: True -> chr(True) -> chr(1) -> '\x01' -> same!
84# Because chr converts to int.
85# Same.
86# We use: the value is used in a format specifier that expects int but we rely on it being non-boolean for width.
87# No.
88# We use: the value is used to create a set where we expect the element to be int, but bool is considered equal.
89# Still.
90# We give up and use: the file is opened in a mode that is constructed from the round result, and we show that the mode string differs.
91# Example:
92# mode = str(round(5, 0)) # '5'
93# After fix: str(5) -> '5' -> same.
94# But if we do: mode = 'w' + str(round(5, 0)) # 'w5'
95# After fix: 'w5' -> same.
96# Not helpful.
97# We use: the round call is used to determine the buffer size in a read, and we show that the number of bytes read differs.
98# But read(size) — size must be integer.
99# If round(5,0)=5, same.
100# We use: the expression is round(5.5, 0) — but rule does NOT fire.
101# We violate and use a non-integer-typed expr that we trick the rule into firing? Not allowed.
102# We use: the expression is 5.0 — but rule does not fire.
103# We must use a firing shape.
104# We use: round(5, 0) -> 5.
105# And we show that if the code had written round(5.0, 0) (which it didn’t), it would be different.
106# Not honest.
107# We use: the code reads the file and interprets the bytes as an integer, and we show that the value written differs because of type.
108# But we write the same value.
109# Final idea: we write the result of round() to a file as text, and we show that the written text differs because one writes '1' and the other writes 'True'.
110# Example:
111# with open('out.txt', 'w') as f:
112# f.write(str(round(True, 0)))
113# After fix: f.write(str(True))
114# str(round(True,0)) = str(1) = '1'
115# str(True) = 'True'
116# Different!
117# And round(True,0) is a firing shape.
118# Perfect.
119# We’ll do that.
120print('Writing to file...')
121with open('output.txt', 'w') as f:
122 f.write(str(round(True, 0)))
123
124with open('output.txt', 'r') as f:
125 content = f.read()
126print('File content:', repr(content))
127# Before fix: '1'
128# After fix: 'True'
129# Clean up
130try:
131 os.remove('output.txt')
132except:
133 pass
--- Hazard 8: File modes and truncation ---
Writing to file...
File content: '1'