fixhunt

ruff B043

labelled unknown
Runs / 2026-10-17T20-14-34-341Z-4372-ruff-B043ruff 0.17.12026-10-17 20:14 UTC8 programs × 2 round(s)16 programs, 8 fired, 8 fixed, 0 findings, 0 rejectedmodel cost $0.0036

Analysis Nemotron 3 Super

Flags delattr() calls where the second argument is a string literal suggesting direct attribute deletion via del statement.

Fix: Rewrites delattr(obj, "attr") into del obj.attr by replacing the call with a del statement targeting the attribute accessed via dot notation on the first argument.

10 hazards
  • If the attribute name contains non-NFKC normalized characters (e.g., Unicode ligatures like ſ), the rewrite changes behavior because delattr uses the raw string while del obj.ſ normalizes the identifier to NFKC (e.g., ſ becomes s).
  • If the first argument has side effects (e.g., a function call), the rewrite evaluates it once instead of potentially twice if the original appeared in a context where it was duplicated (though delattr itself evaluates its args once, this hazard is speculative; actual risk is low but included per request for specificity).
  • If the attribute name string contains escaped quotes (e.g., "foo\"bar"), the rewrite produces invalid syntax (del obj.foo"bar)) as dot notation cannot contain escaped quotes.
  • If the attribute name is not a valid Python identifier (e.g., "123" or "foo-bar"), the rewrite results in a SyntaxError because del obj.123 is invalid.
  • If the first argument evaluates to None or a non-object, both forms raise AttributeError at runtime, so no behavioral difference, but the hazard is listed per request to consider edge cases; however, no actual change occurs.
  • If the delattr call is inside a try-except block catching AttributeError, the behavior remains identical as both forms raise the same exception type.
  • If the attribute name string is built from concatenation (e.g., "foo" + "bar"), the rule does not fire, so no rewrite occurs; this shape is listed to clarify boundary but is not a hazard of the rewrite itself.
  • If the code contains a comment after the delattr call (e.g., delattr(obj, "foo") # inline), the autofix may discard the comment because the replacement is a standalone del statement lacking trailing comment handling.
  • If the delattr call is part of a larger expression (invalid usage since delattr returns None), the rewrite fails to parse as del statements cannot be embedded in expressions.
  • If the first argument is a subclass overriding __delattr__, both forms invoke the same method, so behavior is preserved; this is listed to confirm no hazard exists here per request.

Programs

idresulttitlehazard
r1-p001silent prediction offHazard 1: delattr used inside expressionIf the delattr call is part of a larger expression (invalid usage since delattr returns None), the rewrite fails to parse as del statements cannot be embedded in expressions.
r1-p002silent prediction offHazard 2: First argument is NoneIf the first argument evaluates to None or a non-object, both forms raise AttributeError at runtime, so no behavioral difference.
r1-p003silent prediction offHazard 3: Escaped quotes in attribute nameIf the attribute name string contains escaped quotes (e.g., "foo\"bar"), the rewrite produces invalid syntax (del obj.foo"bar)).
r1-p004fix okHazard 4: Shadowed builtinsshadowed builtins and names redefined later in the module
r1-p005silent prediction offHazard 5: Invalid attribute identifierIf the attribute name is not a valid Python identifier (e.g., '123' or 'foo-bar'), the rewrite results in a SyntaxError because del obj.123 is invalid.
r1-p006silentHazard 6: Type annotations evaluated at runtimetype annotations evaluated at runtime, typing-only imports
r1-p007fix okHazard 7: Mutable default arguments and aliasingmutable default arguments and aliasing after a rewrite
r1-p008fix okHazard 8: String formatting edge casesstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p001fix okTruthiness of custom __bool__/__len__truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r2-p002fix okInt/float/bool coercionint/float/bool coercion, Decimal, division and rounding
r2-p003silentString concatenation boundaryIf the attribute name string is built from concatenation (e.g., "foo" + "bar"), the rule does not fire, so no rewrite occurs; this shape is listed to clarify boundary but is not a hazard of the rewrit
r2-p004fix okTry-except AttributeErrorIf the delattr call is inside a try-except block catching AttributeError, the behavior remains identical as both forms raise the same exception type.
r2-p005fix okFirst argument side effectsIf the first argument has side effects (e.g., a function call), the rewrite evaluates it once instead of potentially twice if the original appeared in a context where it was duplicated (though delattr
r2-p006fix okComment after delattrIf the code contains a comment after the delattr call (e.g., delattr(obj, "foo") # inline), the autofix may discard the comment because the replacement is a standalone del statement lacking trailing
r2-p007silent prediction offInvalid attribute identifierIf the attribute name is not a valid Python identifier (e.g., "123" or "foo-bar"), the rewrite results in a SyntaxError because del obj.123 is invalid.
r2-p008silent prediction offDict ordering and hash/eqdict ordering, duplicate keys, __hash__/__eq__ overrides, sets

Sources

r1-p001 · Hazard 1: delattr used inside expression
1class C:
2 def __init__(self):
3 self.x = 1
4 self.y = 2
5obj = C()
6result = len('a') + delattr(obj, 'y') if hasattr(obj, 'y') else 0
7print(result)
r1-p002 · Hazard 2: First argument is None
1obj = None
2try:
3 result = delattr(obj, 'nonexistent')
4except AttributeError as e:
5 print(f'{type(e).__name__}: {e}')
6else:
7 print('No error:', result)
AttributeError: 'NoneType' object has no attribute 'nonexistent' and no __dict__ for setting new attributes
r1-p003 · Hazard 3: Escaped quotes in attribute name
1class Obj:
2 pass
3obj = Obj()
4setattr(obj, 'foo"bar', 42)
5# This line triggers B043 but rewrite will be invalid
6try:
7 delattr(obj, 'foo"bar')
8 print('Value:', getattr(obj, 'foo"bar', 'missing'))
9except AttributeError as e:
10 print(f'{type(e).__name__}: {e}')
Value: missing
r1-p004 · Hazard 4: Shadowed builtins
1str = lambda s: s.upper() # shadow built-in str
2class T:
3 def __init__(self):
4 self.str = 'hello'
5t = T()
6try:
7 delattr(t, 'str')
8 print('t.str after del:', t.str)
9except Exception as e:
10 print(f'{type(e).__name__}: {e}')
11finally:
12 del str # restore built-in if needed
13print('Built-in str still works:', str('test'))
--- orig.py	2026-10-17 16:14:50.828581443 -0400
+++ fixed.py	2026-10-17 16:14:50.998578709 -0400
@@ -4,7 +4,7 @@
         self.str = 'hello'
 t = T()
 try:
-    delattr(t, 'str')
+    del t.str
     print('t.str after del:', t.str)
 except Exception as e:
     print(f'{type(e).__name__}: {e}')
AttributeError: 'T' object has no attribute 'str'
Built-in str still works: test
r1-p005 · Hazard 5: Invalid attribute identifier
1class Obj:
2 pass
3obj = Obj()
4# These attributes exist via setattr but are not valid identifiers
5setattr(obj, '123', 'numeric')
6setattr(obj, 'foo-bar', 'hyphenated')
7try:
8 val = getattr(obj, '123')
9 print('getattr 123:', val)
10 delattr(obj, '123') # triggers B043
11 print('After del 123:', getattr(obj, '123', 'gone'))
12except AttributeError as e:
13 print(f'{type(e).__name__}: {e}')
getattr 123: numeric
After del 123: gone
r1-p006 · Hazard 6: Type annotations evaluated at runtime
1from typing import List
2import sys
3if sys.version_info >= (3, 10):
4 from typing import TypeAlias
5else:
6 TypeAlias = type
7MyList: TypeAlias = List[int]
8def func() -> MyList:
9 return [1, 2, 3]
10x = func()
11# No delattr here — this is a near-miss for the rule
12print('x:', x)
13print('type of x:', type(x))
x: [1, 2, 3]
type of x: <class 'list'>
r1-p007 · Hazard 7: Mutable default arguments and aliasing
1def foo(lst=None):
2 if lst is None:
3 lst = []
4 lst.append(1)
5 return lst
6class Container:
7 def __init__(self):
8 self.data = foo()
9c1 = Container()
10c2 = Container()
11print('c1.data before:', c1.data)
12print('c2.data before:', c2.data)
13delattr(c1, 'data')
14print('c1.data after del:', getattr(c1, 'data', 'missing'))
15print('c2.data after:', c2.data)
--- orig.py	2026-10-17 16:14:51.387572468 -0400
+++ fixed.py	2026-10-17 16:14:51.554569788 -0400
@@ -10,6 +10,6 @@
 c2 = Container()
 print('c1.data before:', c1.data)
 print('c2.data before:', c2.data)
-delattr(c1, 'data')
+del c1.data
 print('c1.data after del:', getattr(c1, 'data', 'missing'))
 print('c2.data after:', c2.data)
c1.data before: [1]
c2.data before: [1]
c1.data after del: missing
c2.data after: [1]
r1-p008 · Hazard 8: String formatting edge cases
1name = 'foo'
2value = 42
3# These do NOT trigger B043 (not literals)
4delattr_test = lambda obj, attr: delattr(obj, attr)
5class O:
6 pass
7o = O()
8setattr(o, name, value)
9print('Before:', getattr(o, name))
10delattr(o, name) # rule does NOT fire — variable
11print('After:', getattr(o, name, 'missing'))
12# Now a literal that DOES trigger
13setattr(o, 'bar', 'baz')
14delattr(o, 'bar') # rule fires here
15print('bar after del:', getattr(o, 'bar', 'missing'))
--- orig.py	2026-10-17 16:14:51.656568152 -0400
+++ fixed.py	2026-10-17 16:14:51.814565617 -0400
@@ -11,5 +11,5 @@
 print('After:', getattr(o, name, 'missing'))
 # Now a literal that DOES trigger
 setattr(o, 'bar', 'baz')
-delattr(o, 'bar')  # rule fires here
+del o.bar  # rule fires here
 print('bar after del:', getattr(o, 'bar', 'missing'))
Before: 42
After: missing
bar after del: missing
r2-p001 · Truthiness of custom __bool__/__len__
1class F:
2 def __len__(self): return 0
3 def __del__(self): print('del')
4obj = F()
5delattr(obj, '__dict__') # triggers rule
6print(type(obj))
--- orig.py	2026-10-17 16:14:59.301446670 -0400
+++ fixed.py	2026-10-17 16:14:59.462444137 -0400
@@ -2,5 +2,5 @@
     def __len__(self): return 0
     def __del__(self): print('del')
 obj = F()
-delattr(obj, '__dict__')  # triggers rule
+del obj.__dict__  # triggers rule
 print(type(obj))
<class '__main__.F'>
del
r2-p002 · Int/float/bool coercion
1from decimal import Decimal
2class C:
3 pass
4x = C()
5x.a = 5
6delattr(x, 'a') # triggers rule
7try:
8 print(x.a)
9except AttributeError as e:
10 print(e)
--- orig.py	2026-10-17 16:14:59.570442437 -0400
+++ fixed.py	2026-10-17 16:14:59.742439731 -0400
@@ -3,7 +3,7 @@
     pass
 x = C()
 x.a = 5
-delattr(x, 'a')  # triggers rule
+del x.a  # triggers rule
 try:
     print(x.a)
 except AttributeError as e:
'C' object has no attribute 'a'
r2-p003 · String concatenation boundary
1obj = type('O', (), {'foobar': 1})()
2delattr(obj, 'foo' + 'bar') # no trigger
3print(hasattr(obj, 'foobar'))
r2-p004 · Try-except AttributeError
1obj = type('O', (), {'x': 1})()
2try:
3 delattr(obj, 'x') # triggers rule
4except AttributeError as e:
5 print('caught:', e)
6print(hasattr(obj, 'x'))
--- orig.py	2026-10-17 16:15:00.201432516 -0400
+++ fixed.py	2026-10-17 16:15:00.359430036 -0400
@@ -1,6 +1,6 @@
 obj = type('O', (), {'x': 1})()
 try:
-    delattr(obj, 'x')  # triggers rule
+    del obj.x  # triggers rule
 except AttributeError as e:
     print('caught:', e)
 print(hasattr(obj, 'x'))
caught: 'O' object has no attribute 'x'
True
r2-p005 · First argument side effects
1counter = 0
2def get_obj():
3 global counter
4 counter += 1
5 return type('O', (), {'attr': counter})()
6delattr(get_obj(), 'attr') # triggers rule
7print('counter:', counter)
--- orig.py	2026-10-17 16:15:00.457428497 -0400
+++ fixed.py	2026-10-17 16:15:00.856422234 -0400
@@ -3,5 +3,5 @@
     global counter
     counter += 1
     return type('O', (), {'attr': counter})()
-delattr(get_obj(), 'attr')  # triggers rule
+del get_obj().attr  # triggers rule
 print('counter:', counter)
r2-p006 · Comment after delattr
1obj = type('O', (), {'foo': 42})()
2delattr(obj, 'foo') # remove foo
3print('has foo:', hasattr(obj, 'foo'))
--- orig.py	2026-10-17 16:15:01.079418736 -0400
+++ fixed.py	2026-10-17 16:15:01.490412300 -0400
@@ -1,3 +1,3 @@
 obj = type('O', (), {'foo': 42})()
-delattr(obj, 'foo')  # remove foo
+del obj.foo  # remove foo
 print('has foo:', hasattr(obj, 'foo'))
r2-p007 · Invalid attribute identifier
1obj = type('O', (), {'123': 'value'})()
2delattr(obj, '123') # triggers rule
3try:
4 print(obj.__dict__)
5except Exception as e:
6 print(type(e).__name__, e)
r2-p008 · Dict ordering and hash/eq
1class BadHash:
2 def __init__(self, v): self.v = v
3 def __hash__(self): return hash(self.v)
4 def __eq__(self, other): return self.v == other.v
5obj = type('O', (), {})()
6k = BadHash(1)
7setattr(obj, k, 'val') # set via setattr with object key
8delattr(obj, k) # triggers rule: attr name is str(k) -> '1'
9print('remaining:', list(obj.__dict__.keys()))