ruff PLW0245
labelled unknown
Analysis Nemotron 3 Super
Flags uses of 'super' without parentheses when attempting to call it like a function.
Fix: Rewrites 'super' to 'super()' when followed by attribute access, subscript, or call, preserving all arguments and whitespace.
8 hazards
- Changing 'super.method()' to 'super().method()' alters the MRO resolution if 'super' is shadowed by a local variable named 'super'.
- If 'super' is a property or descriptor with side effects, accessing it once vs. calling it changes behavior.
- In 'super[index]', rewriting to 'super()[index]' may fail if the super proxy object is not subscriptable.
- For 'super(*args)' or 'super(**kwargs)', the rewrite creates a super proxy then calls it with args, which differs from calling the original 'super' callable with those arguments.
- If 'super' is imported or aliased (e.g., 'from builtins import super as s'), the fix may incorrectly apply to the alias.
- In nested scopes where 'super' is rebound locally, the fix ignores the local binding and always inserts parentheses.
- Comments between 'super' and the following dot/bracket/paren may be lost or misplaced during rewrite.
- If 'super' is used in a string or comment-like context (e.g., r'super.method'), the tool may incorrectly modify it due to faulty parsing.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Hazard 1: Local shadowing changes MRO | Changing 'super.method()' to 'super().method()' alters the MRO resolution if 'super' is shadowed by a local variable named 'super'. |
| r1-p002 | silent prediction off | Hazard 2: Aliased super misapplied | If 'super' is imported or aliased (e.g., 'from builtins import super as s'), the fix may incorrectly apply to the alias. |
| r1-p003 | silent | Hazard 3: String/comment false positive | If 'super' is used in a string or comment-like context (e.g., r'super.method'), the tool may incorrectly modify it due to faulty parsing. |
| r1-p004 | silent prediction off | Hazard 4: Encoding and newline handling | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p005 | silent prediction off | Hazard 5: super[index] not subscriptable | In 'super[index]', rewriting to 'super()[index]' may fail if the super proxy object is not subscriptable. |
| r1-p006 | silent prediction off | Hazard 6: super(*args) callable difference | For 'super(*args)' or 'super(**kwargs)', the rewrite creates a super proxy then calls it with args, which differs from calling the original 'super' callable with those arguments. |
| r1-p007 | silent prediction off | Hazard 7: Walrus and comprehension scoping | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p008 | invalid | Hazard 8: Mutable defaults and aliasing after rewrite | mutable default arguments and aliasing after a rewrite |
| r2-p001 | silent prediction off | Property/side-effect descriptor | If 'super' is a property or descriptor with side effects, accessing it once vs. calling it changes behavior. |
| r2-p002 | silent prediction off | Bytes vs str encoding/newline | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p003 | silent prediction off | Local shadowing changes MRO | Changing 'super.method()' to 'super().method()' alters the MRO resolution if 'super' is shadowed by a local variable named 'super'. |
| r2-p004 | silent prediction off | pathlib vs os.path behavior | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r2-p005 | silent prediction off | Shadowed builtins and redefinition | shadowed builtins and names redefined later in the module |
| r2-p006 | silent | String/comment-like context | If 'super' is used in a string or comment-like context (e.g., r'super.method'), the tool may incorrectly modify it due to faulty parsing. |
| r2-p007 | silent prediction off | Non-file context managers | context managers that are not files, objects with write() but no close() |
| r2-p008 | silent prediction off | Dict ordering, duplicates, overrides | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
Sources
r1-p001 · Hazard 1: Local shadowing changes MRO
1class A:
2 def method(self): return 'A'
3class B(A):
4 def method(self):
5 super = lambda: None # shadow
6 try:
7 return super.method() # triggers rule: should become super().method()
8 except AttributeError as e:
9 return f'AttributeError: {e}'
10 except TypeError as e:
11 return f'TypeError: {e}'
12print(B().method())
AttributeError: 'function' object has no attribute 'method'
r1-p002 · Hazard 2: Aliased super misapplied
1from builtins import super as s
2class A:
3 def attr(self): return 'A_attr'
4class B(A):
5 def attr(self):
6 return s.attr # triggers rule on 's.attr'? NO — but if rule mistakenly sees 's' as super, it breaks
7try:
8 print(B().attr()) # prints 'A_attr' if correct; fails if rule broke s into s()
9except Exception as e:
10 print(f'{type(e).__name__}: {e}')
AttributeError: type object 'super' has no attribute 'attr'
r1-p003 · Hazard 3: String/comment false positive
1code = r'super.method()'
2# This line contains super.method() — should NOT be changed
3if 'super.method()' in code:
4 print('found in string')
5else:
6 print('not found')
7print(code)
found in string super.method()
r1-p004 · Hazard 4: Encoding and newline handling
1class A:
2 def __str__(self): return 'A_str'
3 def __bytes__(self): return b'A_bytes'
4class B(A):
5 def method(self):
6 s = super.__str__ # triggers rule: becomes super().__str__()
7 if callable(s):
8 return s()
9 else:
10 return str(s)
11print(B().method()) # Should print 'A_str'; if broken, returns '<built-in method ...>'
r1-p005 · Hazard 5: super[index] not subscriptable
1class A: pass
2class B(A):
3 def get(self):
4 try:
5 return super[0] # triggers rule: becomes super()[0]
6 except TypeError as e:
7 return f'TypeError: {e}'
8print(B().get()) # Should print TypeError: 'super' object is not subscriptable
TypeError: type 'super' is not subscriptable
r1-p006 · Hazard 6: super(*args) callable difference
1class A:
2 def hello(self, x): return f'A:{x}'
3class B(A):
4 def hello(self, x):
5 # super is a callable that returns a super proxy; super(x) tries to call proxy with x -> error
6 # super() returns proxy, then super()(x) tries to call proxy with x -> same error?
7 # Actually: super is NOT callable in this context — super() is the proxy.
8 # But the *original* super (without parens) IS callable: it returns a super proxy.
9 # So super(x) == super()(x) ???
10 # Let's test:
11 try:
12 return super(x) # triggers rule: becomes super()(x)
13 except Exception as e:
14 return f'{type(e).__name__}: {e}'
15print(B().hello(42)) # Both should fail same way — but if rule misfires on non-call? Wait.
16# Actually: super is always a callable (built-in type). super(x) is valid: returns super proxy for MRO starting after x.
17# super() is invalid: no args -> proxy for self.__class__. Then super()(x) tries to call proxy with x -> TypeError.
18# So: super(42) works; super()(42) fails.
TypeError: super() argument 1 must be a type, not int
r1-p007 · Hazard 7: Walrus and comprehension scoping
1class A:
2 def m(self): return 1
3class B(A):
4 def m(self):
5 f = lambda: (s := super).m() # triggers rule on 's.m()'? s is lambda return — no.
6 # Let's put trigger inside lambda:
7 g = lambda: super.m() # triggers: becomes super().m()
8 h = lambda: (yield super.m()) # same
9 # But we need side effect: capture changing super
10 shadows = []
11 for i in range(2):
12 if i == 1:
13 super = 99 # shadow in loop
14 shadows.append(lambda: super.m() if callable(super) else f'shadow:{super}')
15 return shadows[1]() # second lambda captures i=1 -> super=99
16print(B().m()) # If rule fired on super.m() in lambda, it becomes super().m() -> still uses closed-over super (99) -> AttributeError
17# But if rule did NOT fire, super.m() uses the built-in super -> 1
18# We want: if rule fires, we get AttributeError (because 99 has no m); if not, we get 1.
19# However, the lambda is defined in loop scope — super is looked up at call time.
20# At i=1, super=99 in loop, so lambda sees super=99.
21# If rule rewrote super.m() to super().m(), then at call: super() -> 99() -> TypeError: 'int' not callable
22# If not rewritten: super.m() -> 99.m() -> AttributeError
23# So we distinguish:
shadow:99
r1-p008 · Hazard 8: Mutable defaults and aliasing after rewrite (generated program does not parse: Traceback (most recent call last):)
1class A:
2 def vals(self): return []
3class B(A):
4 def vals(self, cache=[]):
5 cache.append(super.vals()) # triggers rule: becomes super().vals()
6 return cache
7print(B().vals()) # First call: [ [] ]
8print(B().vals()) # Second call: [ [], [] ] if super is B's super
9# But if rule rewrites and super is shadowed?
10# Let's add shadow after default is set:
11shadow = super # save original super
12super = lambda: None # shadow
13# Now second call uses shadowed super
14try:
15 print(B().vals()) # super.vals() -> shadow().vals() -> None.vals() -> AttributeError
16# But first call used original super (because default arg evaluated at def time)
17# So: first print: [ [] ]
18# second print: AttributeError
19# If rule did NOT fire: first: [ [] ], second: super.vals() -> shadow.vals() -> AttributeError (same)
20# Wait — need to see if rewrite changes behavior.
21# Original: super.vals() -> calls A.vals() -> []
22# Rewritten: super().vals() -> calls A.vals() -> [] (same!)
23# So no difference unless super is not the built-in.
24# Let's make super a class with vals that returns something else.
25# Redefine:
26class SuperMock:
27 @staticmethod
28 def vals(): return ["mock"]
29super = SuperMock # now super is a class
30print('First:', B().vals()) # super.vals() -> SuperMock.vals() -> ['mock']
31print('Second:', B().vals()) # cache now has [['mock']], then appends SuperMock.vals() -> ['mock'] -> [['mock'], ['mock']]
32# If rule rewrote: super().vals() -> SuperMock().vals() -> but SuperMock has no instance vals -> AttributeError
33# So:
34# No rewrite: ['mock'] then [['mock'], ['mock']]
35# With rewrite: First call: AttributeError on SuperMock().vals()
36}
r2-p001 · Property/side-effect descriptor
1class Meta(type):
2 def __get__(cls, obj, typ=None):
3 print('get', end='')
4 return super
5 def __call__(cls, *a, **kw):
6 print('call', end='')
7 return super().__call__(*a, **kw)
8class A(metaclass=Meta):
9 pass
10try:
11 A.super.method() # triggers PLW0245 -> becomes A.super().method()
12except AttributeError as e:
13 print(f'AttributeError: {e}')
AttributeError: type object 'A' has no attribute 'super'
r2-p002 · Bytes vs str encoding/newline
1import io
2buf = io.BytesIO()
3buf.write(b'super\nline2')
4buf.seek(0)
5line = buf.readline() # b'super\n'
6if line.startswith(b'super'):
7 try:
8 line.super.decode() # triggers PLW0245 -> line.super().decode()
9 except AttributeError as e:
10 print(f'AttributeError: {e}')
11else:
12 print('no match')
AttributeError: 'bytes' object has no attribute 'super'
r2-p003 · Local shadowing changes MRO
1def f():
2 super = lambda: None # local shadows builtin
3 try:
4 super.__class__() # triggers PLW0245 -> super().__class__()
5 except AttributeError as e:
6 print(f'AttributeError: {e}')
7f()
r2-p004 · pathlib vs os.path behavior
1import os
2try:
3 open('tmp_super_test.txt', 'w').close()
4 p = os.path.join('tmp_super_test.txt')
5 if hasattr(p, 'super'): # false, but imagine proxy
6 p.super.stat() # triggers PLW0245 -> p.super().stat()
7 else:
8 print('no super attr')
9except Exception as e:
10 print(f'{type(e).__name__}: {e}')
11finally:
12 try: os.remove('tmp_super_test.txt')
13 except: pass
no super attr
r2-p005 · Shadowed builtins and redefinition
1super = 1
2if super == 1:
3 super = lambda: None # rebind
4try:
5 super.__name__() # triggers PLW0245 -> super().__name__()
6except AttributeError as e:
7 print(f'AttributeError: {e}')
r2-p006 · String/comment-like context
1r = r'super.method()' # raw string, should not trigger
2s = 'super.attr'
3c = super # real use
4print(f'r={r}, s={s}, c={c}')
r=super.method(), s=super.attr, c=<class 'super'>
r2-p007 · Non-file context managers
1class Log:
2 def __enter__(self): return self
3 def __exit__(self, *a): pass
4 def write(self, msg): print(f'LOG: {msg}')
5with Log() as log:
6 if hasattr(log, 'super'):
7 log.super.write('test') # triggers PLW0245 -> log.super().write('test')
8 else:
9 print('no super attr')
no super attr
r2-p008 · Dict ordering, duplicates, overrides
1d = {'a': 1, 'b': 2}
2d['super'] = lambda: None
3try:
4 d['super'].__call__() # triggers PLW0245 -> d['super']().__call__()
5except AttributeError as e:
6 print(f'AttributeError: {e}')
7print(len(d))
3