ruff B043
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Hazard 1: delattr used inside expression | 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. |
| r1-p002 | silent prediction off | Hazard 2: First argument is None | If the first argument evaluates to None or a non-object, both forms raise AttributeError at runtime, so no behavioral difference. |
| r1-p003 | silent prediction off | Hazard 3: Escaped quotes in attribute name | If the attribute name string contains escaped quotes (e.g., "foo\"bar"), the rewrite produces invalid syntax (del obj.foo"bar)). |
| r1-p004 | fix ok | Hazard 4: Shadowed builtins | shadowed builtins and names redefined later in the module |
| r1-p005 | silent prediction off | Hazard 5: Invalid attribute identifier | 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. |
| r1-p006 | silent | Hazard 6: Type annotations evaluated at runtime | type annotations evaluated at runtime, typing-only imports |
| r1-p007 | fix ok | Hazard 7: Mutable default arguments and aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p008 | fix ok | Hazard 8: String formatting edge cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p001 | fix ok | Truthiness of custom __bool__/__len__ | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r2-p002 | fix ok | Int/float/bool coercion | int/float/bool coercion, Decimal, division and rounding |
| r2-p003 | silent | String concatenation boundary | 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 rewrit |
| r2-p004 | fix ok | Try-except AttributeError | If the delattr call is inside a try-except block catching AttributeError, the behavior remains identical as both forms raise the same exception type. |
| r2-p005 | fix ok | First argument side effects | 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 |
| r2-p006 | fix ok | Comment after delattr | 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 |
| r2-p007 | silent prediction off | Invalid attribute identifier | 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. |
| r2-p008 | silent prediction off | Dict ordering and hash/eq | dict 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()))