ruff FURB162
labelled unknown
Analysis Nemotron 3 Super
Flags `datetime.fromisoformat()` called with a string where `.replace('Z', '+00:00')` has been applied to remove a trailing 'Z' suffix.
Fix: Replace `datetime.fromisoformat(arg)` with `datetime.fromisoformat(base_string)`, where `arg` is a transformation like `s.replace("Z", "+00:00")`, `t[:-1] + "-00"`, etc., and `base_string` is the original variable before transformation.
11 hazards
- If the base string contains internal 'Z' characters (e.g., "Z2025-01-01T00:00:00Z"), removing them via replace changes the input meaning and may cause parsing errors.
- If the base string ends with multiple 'Z' characters, slicing off only one character (e.g., `[:-1]`) leaves trailing 'Z's, resulting in invalid ISO format.
- If the base string lacks a trailing 'Z', transformations like `rstrip("Z", "-00:00")` may still alter the string (e.g., removing trailing zeros), producing a different datetime.
- If the base string is not a literal but a function call or complex expression, the rewrite may alter evaluation order or side effects if the transformation had any (though pure string methods are side-effect free, the base expression is evaluated once vs potentially multiple times in the original).
- If the variable naming is reused or aliased elsewhere in the scope, the rewrite assumes the base variable is unchanged between its use in the transformation and the fromisoformat call, which may not hold if mutated in between (though unlikely for strings).
- If the code relies on the transformation to normalize case (e.g., expecting lowercase 'z'), the fix does not account for that and may fail to parse.
- If the string contains timezone offsets other than UTC (e.g., "+05:30"), the transformation incorrectly forces UTC, changing the represented time.
- If the code is run under Python <3.11, `fromisoformat` cannot parse the 'Z' suffix directly, so the fix would cause a ValueError where the original code worked.
- If the transformation includes additional logic beyond the simple patterns (e.g., chained replaces), the tool may not trigger, but if it did incorrectly, the fix would drop meaningful processing.
- Comments attached to the transformation expression (e.g., `# workaround`) would be lost when the entire argument expression is replaced.
- If the base string variable is defined later in the block (due to hoisting-like confusion in conditional blocks), the rewrite could reference it before assignment.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Truthiness comparison | __bool__ / __len__, None vs falsy, 'is' vs '==' |
| r1-p002 | silent prediction off | Chained replace drops meaning | If the transformation includes additional logic beyond the simple patterns, the fix would drop meaningful processing. |
| r1-p003 | fired, no fix | Non-UTC offset corrupted | If the string contains timezone offsets other than UTC, the transformation incorrectly forces UTC, changing the represented time. |
| r1-p004 | fired, no fix | Comment lost in fix | Comments attached to the transformation expression would be lost when the entire argument expression is replaced. |
| r1-p005 | silent | Coercion edge case | int/float/bool coercion, Decimal, division and rounding |
| r1-p006 | silent prediction off | Multiple trailing Z | If the base string ends with multiple 'Z' characters, slicing off only one character leaves trailing 'Z's, resulting in invalid ISO format. |
| r1-p007 | fired, no fix | Async interaction | async def/await ordering, async generators |
| r1-p008 | fired, no fix | Base string used before assignment | If the base string variable is defined later in the block, the rewrite could reference it before assignment. |
| r2-p001 | fired, no fix | shadowed builtins and names redefined later | shadowed builtins and names redefined later in the module |
| r2-p002 | silent prediction off | int/float/bool coercion, Decimal, division and rounding | int/float/bool coercion, Decimal, division and rounding |
| r2-p003 | fired, no fix | multiple trailing Z characters | If the base string ends with multiple 'Z' characters, slicing off only one character (e.g., `[:-1]`) leaves trailing 'Z's, resulting in invalid ISO format. |
| r2-p004 | fired, no fix | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r2-p005 | fired, no fix | base string defined later in the block | If the base string variable is defined later in the block (due to hoisting-like confusion in conditional blocks), the rewrite could reference it before assignment. |
| r2-p006 | fired, no fix | comments attached to the transformation expression | Comments attached to the transformation expression (e.g., `# workaround`) would be lost when the entire argument expression is replaced. |
| r2-p007 | fired, no fix | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p008 | fired, no fix | class attributes vs instance attributes, descriptors, properties with side effects | class attributes vs instance attributes, descriptors, properties with side effects |
Sources
r1-p001 · Truthiness comparison
1from datetime import datetime
2
3def f():
4 class X:
5 def __init__(self, v):
6 self.v = v
7 def __bool__(self):
8 return self.v != ''
9 def __len__(self):
10 return len(self.v)
11 s = X('2023-01-01T12:00:00Z')
12 t = datetime.fromisoformat(s.replace("Z", "+00:00"))
13 u = datetime.fromisoformat(str(s))
14 print('t:', t.isoformat())
15 print('u:', u.isoformat())
16 print('equal?', t == u)
17 print('same?', t is u)
18f()
r1-p002 · Chained replace drops meaning
1from datetime import datetime
2s = '2023-01-01T12:00:00Z'
3t = s.replace("T", "_").replace("Z", "+00:00")
4try:
5 dt = datetime.fromisoformat(t)
6 print('parsed:', dt.isoformat())
7except Exception as e:
8 print(type(e).__name__, ':', e)
parsed: 2023-01-01T12:00:00+00:00
r1-p003 · Non-UTC offset corrupted
1from datetime import datetime
2s = '2023-01-01T12:00:00+05:30'
3dt_wrong = datetime.fromisoformat(s.replace("Z", "+00:00"))
4dt_right = datetime.fromisoformat(s)
5print('wrong:', dt_wrong.isoformat())
6print('right:', dt_right.isoformat())
7print('diff hours:', (dt_right - dt_wrong).total_seconds() / 3600)
wrong: 2023-01-01T12:00:00+05:30 right: 2023-01-01T12:00:00+05:30 diff hours: 0.0
r1-p004 · Comment lost in fix
1from datetime import datetime
2# input from user
3s = '2023-01-01T12:00:00Z' # Zulu time
4try:
5 dt = datetime.fromisoformat(s.replace("Z", "+00:00")) # workaround
6 print('ok:', dt.isoformat())
7except Exception as e:
8 print(type(e).__name__, ':', e)
ok: 2023-01-01T12:00:00+00:00
r1-p005 · Coercion edge case
1from datetime import datetime
2from decimal import Decimal
3s = '2023-01-01T12:00:00'
4val = Decimal('10.5') // 2
5try:
6 dt = datetime.fromisoformat(s + str(int(val)))
7 print('dt:', dt.isoformat())
8except Exception as e:
9 print(type(e).__name__, ':', e)
ValueError : Invalid isoformat string: '2023-01-01T12:00:005'
r1-p006 · Multiple trailing Z
1from datetime import datetime
2s = '2023-01-01T12:00:00ZZ'
3t = s[:-1] + '-00'
4try:
5 dt = datetime.fromisoformat(t)
6 print('parsed:', dt.isoformat())
7except Exception as e:
8 print(type(e).__name__, ':', e)
ValueError : Invalid isoformat string: '2023-01-01T12:00:00Z-00'
r1-p007 · Async interaction
1from datetime import datetime
2import asyncio
3
4async def gen():
5 yield '2023-01-01T12:00:00Z'
6
7async def main():
8 async for s in gen():
9 dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
10 print('async parsed:', dt.isoformat())
11asyncio.run(main())
async parsed: 2023-01-01T12:00:00+00:00
r1-p008 · Base string used before assignment
1from datetime import datetime
2if True:
3 try:
4 dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
5 print('dt:', dt.isoformat())
6 except NameError as e:
7 print('NameError:', e)
8 s = '2023-01-01T12:00:00Z'
NameError: name 's' is not defined
r2-p001 · shadowed builtins and names redefined later
1import datetime
2str = lambda x: x.upper()
3# str now shadows built-in
4val = '2024-01-01T12:00:00Z'
5tried = datetime.datetime.fromisoformat(str(val).replace('Z', '+00:00')) # rule should fire here
6print('parsed:', tried)
7del str
8try:
9 datetime.datetime.fromisoformat('invalid'.replace('Z', '+00:00'))
10except Exception as e:
11 print('error after del:', type(e).__name__, ':', e)
parsed: 2024-01-01 12:00:00+00:00 error after del: ValueError : Invalid isoformat string: 'invalid'
r2-p002 · int/float/bool coercion, Decimal, division and rounding
1from datetime import datetime
2import decimal
3x = '2024-01-01T00:00:00Z'
4y = bool(x) # True
5z = decimal.Decimal('42') / decimal.Decimal('2') # 21.0
6# Expression with coercion side effects
7ts = datetime.fromisoformat((x.replace('Z', '+00:00'), int(z))[0]) # tuple indexing, replace inside
8print('parsed:', ts)
9print('y:', y, 'z:', z)
parsed: 2024-01-01 00:00:00+00:00 y: True z: 21
r2-p003 · multiple trailing Z characters
1from datetime import datetime
2s = '2024-01-01T00:00:00ZZ' # two Z's
3# Rule triggers on s[:-1] + '-00' but leaves one Z
4try:
5 dt = datetime.fromisoformat(s[:-1] + '-00') # becomes ...00:00Z-00 -> invalid
6 print('unexpected success:', dt)
7except ValueError as e:
8 print('ValueError (expected):', e)
9# Correct would be s.replace('Z', '+00:00') or strip all trailing Z
10s_clean = s.rstrip('Z') + '+00:00'
11dt2 = datetime.fromisoformat(s_clean)
12print('correct parse:', dt2)
ValueError (expected): Invalid isoformat string: '2024-01-01T00:00:00Z-00' correct parse: 2024-01-01 00:00:00+00:00
r2-p004 · truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
1from datetime import datetime
2class MockStr:
3 def __init__(self, s):
4 self.s = s
5 def replace(self, a, b):
6 print('replace called')
7 return self.s.replace(a, b)
8 def __bool__(self):
9 return len(self.s) > 10
10 def __len__(self):
11 return len(self.s)
12s = MockStr('2024-01-01T00:00:00Z')
13print('bool(s):', bool(s))
14if s: # uses __bool__
15 dt = datetime.fromisoformat(s.replace('Z', '+00:00')) # rule fires here
16 print('parsed:', dt)
17else:
18 print('false branch')
bool(s): True replace called parsed: 2024-01-01 00:00:00+00:00
r2-p005 · base string defined later in the block
1from datetime import datetime
2if False:
3 s = '2024-01-01T00:00:00Z' # not executed
4else:
5 s = '2024-01-01T00:00:00Z' # real definition
6# s is defined, but imagine if rule wrongly hoisted
7try:
8 dt = datetime.fromisoformat(s.replace('Z', '+00:00')) # rule fires
9 print('parsed:', dt)
10except NameError as e:
11 print('NameError (should not happen):', e)
12print('s is:', s)
parsed: 2024-01-01 00:00:00+00:00 s is: 2024-01-01T00:00:00Z
r2-p006 · comments attached to the transformation expression
1from datetime import datetime
2s = '2024-01-01T00:00:00Z'
3# Rule should fire on next line, comment must be preserved in spirit via output
4dt = datetime.fromisoformat(s.replace('Z', '+00:00')) # workaround for Zulu time
5print('parsed:', dt)
6# Simulate comment loss by checking if we can still associate intent
7print('workaround applied: Zulu time converted')
parsed: 2024-01-01 00:00:00+00:00 workaround applied: Zulu time converted
r2-p007 · keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
1from datetime import datetime
2def parse_time(*, source, fix_z=True):
3 if fix_z:
4 # Rule fires on next line inside function
5 return datetime.fromisoformat(source.replace('Z', '+00:00'))
6 return datetime.fromisoformat(source)
7s = '2024-01-01T00:00:00Z'
8print(parse_time(source=s))
9print(parse_time(source=s, fix_z=False))
2024-01-01 00:00:00+00:00 2024-01-01 00:00:00+00:00
r2-p008 · class attributes vs instance attributes, descriptors, properties with side effects
1from datetime import datetime
2class Timestamp:
3 def __init__(self, s):
4 self._s = s
5 @property
6 def iso(self):
7 print('property accessed')
8 return self._s
9 @iso.setter
10 def iso(self, v):
11 self._s = v
12 def parse(self):
13 # Rule fires on next line: instance property access in attribute
14 return datetime.fromisoformat(self.iso.replace('Z', '+00:00'))
15ts = Timestamp('2024-01-01T00:00:00Z')
16print('parsed:', ts.parse())
17# Trigger again via reassignment
18ts.iso = '2024-01-02T00:00:00Z'
19print('re-parsed:', ts.parse())
property accessed parsed: 2024-01-01 00:00:00+00:00 property accessed re-parsed: 2024-01-02 00:00:00+00:00