ruff F504
labelled unknown
Analysis Nemotron 3 Super
Flags printf-style formatting expressions where extra named keys appear in the mapping argument but have no corresponding placeholder in the format string.
Fix: Removes all key-value pairs from the mapping argument where the key does not appear as a named placeholder in the format string.
12 hazards
- Removing a key whose value is a function call (e.g., func()) eliminates its side effects, changing program behavior if the function performs I/O or mutates state.
- If the mapping argument is a variable (not a literal), the fix incorrectly assumes it can be rewritten as a smaller literal, which fails when the variable is reused elsewhere or has dynamic contents.
- When the format string contains duplicate placeholders (e.g., '%(a)s %(a)s'), removing a key that appears only once in the mapping but is referenced multiple times may still be safe, but the fix logic must not overremove; however, the rule does not fire in such cases because the key is used.
- If the mapping argument is constructed via a function call (e.g., % build_dict()), the fix cannot safely rewrite it to a literal without knowing the function's output, potentially causing a mismatch between expected and actual keys.
- If the mapping argument includes nested expressions like %(key)s % {'a': f().b}, removing the key may break attribute access if f() returns None or an unexpected type, though this is less common.
- Comments attached to the removed key-value pairs (e.g., # TODO) are lost when the fix rewrites the dictionary literal, stripping inline documentation.
- In Python versions prior to 3.6, dictionary literals preserve insertion order only as an implementation detail; removing keys may alter iteration order if the dict is later passed to code relying on order (e.g., for k, v in d.items(): ...)
- If the mapping argument is used in a larger expression (e.g., (%(a)s) % {'a':1, 'b':2} + c), the fix rewrites only the dict part, but if the surrounding expression depends on the exact structure (e.g., len(...) or == comparison), behavior may change.
- When the format string uses mixed formatting (e.g., '%(name)s %d' % ({'name': 'x', 'extra': y}, 42)), the fix incorrectly assumes the entire right-hand side is a mapping, leading to invalid syntax or TypeError.
- If the mapping argument is a subclass of dict that overrides __missing__ or __getitem__, removing keys may change behavior if the code relies on the fallback mechanism for missing keys.
- The fix may produce a dictionary with fewer keys than expected by subsequent code that inspects the argument (e.g., via locals() or vars()), breaking introspection or debugging logic.
- In rare cases, removing a key could resolve a naming conflict in a comprehension or lambda scope if the key was accidentally shadowing a built-in, though this is highly speculative and context-dependent.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Variable reuse after removal | If the mapping argument is a variable (not a literal), the fix incorrectly assumes it can be rewritten as a smaller literal, which fails when the variable is reused elsewhere or has dynamic contents. |
| r1-p002 | silent prediction off | File write partial failure | file modes and truncation, partial writes, non-str data passed to write |
| r1-p003 | silent prediction off | Shadowed built-in redefinition | shadowed builtins and names redefined later in the module |
| r1-p004 | silent prediction off | Walrus and late-binding closure | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p005 | silent prediction off | Property with side effect in dict | class attributes vs instance attributes, descriptors, properties with side effects |
| r1-p006 | silent prediction off | Dict in larger expression with len() | If the mapping argument is used in a larger expression (e.g., (%(a)s) % {'a':1, 'b':2} + c), the fix rewrites only the dict part, but if the surrounding expression depends on the exact structure (e.g. |
| r1-p007 | silent prediction off | Lost inline comment in dict literal | Comments attached to the removed key-value pairs (e.g., # TODO) are lost when the fix rewrites the dictionary literal, stripping inline documentation. |
| r1-p008 | silent prediction off | Accidental built-in shadow resolved by removal | In rare cases, removing a key could resolve a naming conflict in a comprehension or lambda scope if the key was accidentally shadowing a built-in, though this is highly speculative and context-depende |
| r2-p001 | silent | Duplicate placeholders - rule should not fire since key is used | When the format string contains duplicate placeholders (e.g., '%(a)s %(a)s'), removing a key that appears only once in the mapping but is referenced multiple times may still be safe, but the fix logic |
| r2-p002 | silent prediction off | int/float/bool coercion, Decimal, division and rounding | int/float/bool coercion, Decimal, division and rounding |
| r2-p003 | silent prediction off | Nested expressions in mapping value breaking on attribute access | If the mapping argument includes nested expressions like %(key)s % {'a': f().b}, removing the key may break attribute access if f() returns None or an unexpected type, though this is less common. |
| r2-p004 | silent prediction off | Keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p005 | silent prediction off | Mixed formatting with tuple RHS - fix assumes mapping only | When the format string uses mixed formatting (e.g., '%(name)s %d' % ({'name': 'x', 'extra': y}, 42)), the fix incorrectly assumes the entire right-hand side is a mapping, leading to invalid syntax or |
| r2-p006 | silent prediction off | File modes and truncation, partial writes, non-str data passed to write | file modes and truncation, partial writes, non-str data passed to write |
| r2-p007 | silent prediction off | Walrus, comprehensions scoping, late binding in closures and lambdas | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p008 | silent prediction off | String formatting edge cases: %, format, f-strings, repr vs str, nested quotes | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
Sources
r1-p001 · Variable reuse after removal
1mapping = {'x': 'keep', 'y': 'remove'} # y unused
2result = '%(x)s' % mapping
3print('First:', result)
4mapping['y'] = 'changed'
5result = '%(x)s' % mapping
6print('Second:', result)
First: keep Second: keep
r1-p002 · File write partial failure
1f = open('test.txt', 'w')
2data = {'msg': 'hello', 'junk': 123} # junk unused
3try:
4 f.write('%(msg)s\n' % data)
5except Exception as e:
6 print('Write error:', type(e).__name__, str(e))
7f.close()
8with open('test.txt', 'r') as f:
9 print('File content:', repr(f.read()))
File content: 'hello\n'
r1-p003 · Shadowed built-in redefinition
1str = 'overridden' # shadow built-in
2mapping = {'a': 'value', 'str': 'also_used'} # str used in format
3fmt = '%(a)s %(str)s'
4result = fmt % mapping
5print('Result:', result)
6# Later, str is reused as built-in via recovery
7try:
8 s = str([1, 2, 3])
9except Exception as e:
10 print('str recovery failed:', type(e).__name__, str(e))
11else:
12 print('str recovery:', s)
Result: value also_used
r1-p004 · Walrus and late-binding closure
1funcs = []
2for i in range(2):
3 mapping = {'a': i, 'b': i*2} # b unused
4 if (s := mapping.get('a')) is not None:
5 funcs.append(lambda m=mapping: '%(a)s' % m) # uses mapping
6for f in funcs:
7 print('Closure result:', f())
Closure result: 0 Closure result: 1
r1-p005 · Property with side effect in dict
1class Data:
2 def __init__(self):
3 self._val = 0
4 @property
5 def val(self):
6 self._val += 1
7 return self._val
8obj = Data()
9mapping = {'value': obj.val, 'extra': obj.val} # extra unused but triggers side effect
10result = 'Value: %(value)d' % mapping
11print('Result:', result)
12print('Obj val after:', obj.val)
Result: Value: 1 Obj val after: 3
r1-p006 · Dict in larger expression with len()
1mapping = {'a': 'hello', 'b': 'world'} # b unused
2part = 'Hello %(a)s' % mapping
3combined = part + str(len(mapping)) # len depends on full dict
4print('Combined:', combined)
5# After fix: mapping becomes {'a': 'hello'}, len=1 -> 'Hello hello1'
6# Before: len=2 -> 'Hello hello2'
Combined: Hello hello2
r1-p007 · Lost inline comment in dict literal
1# This comment is outside
2mapping = {
3 'keep': 'value', # Important
4 'remove': 42, # TODO: remove this
5}
6result = 'Keep: %(keep)s' % mapping
7print('Result:', result)
8print('Mapping keys:', list(mapping.keys())) # Should show both keys before fix
Result: Keep: value Mapping keys: ['keep', 'remove']
r1-p008 · Accidental built-in shadow resolved by removal
1list = [1, 2, 3] # shadow built-in
2mapping = {'x': 'test', 'list': [4, 5]} # list unused in format, but shadows
3# Format uses only 'x', so 'list' key is superfluous
4result = 'X: %(x)s' % mapping
5print('Result:', result)
6# After fix, mapping loses 'list' key -> list built-in accessible again?
7# But we don't rely on that; instead, we show that 'list' var still shadows
8try:
9 x = list(range(2)) # This should fail because list is shadowed
10 print('list() worked:', x)
11except Exception as e:
12 print('list() failed:', type(e).__name__, str(e))
Result: X: test list() failed: TypeError 'list' object is not callable
r2-p001 · Duplicate placeholders - rule should not fire since key is used
1template = '%(a)s %(a)s'
2data = {'a': 42, 'unused': 'should_not_be_removed_by_rule'}
3result = template % data
4print('Output:', result)
Output: 42 42
r2-p002 · int/float/bool coercion, Decimal, division and rounding
1from decimal import Decimal
2def make_val():
3 return Decimal('10.5')
4template = 'Value: %(val).2f'
5data = {'val': make_val(), 'extra': 3 / 2}
6try:
7 result = template % data
8 print('Formatted:', result)
9except Exception as e:
10 print('Error:', type(e).__name__, str(e))
Formatted: Value: 10.50
r2-p003 · Nested expressions in mapping value breaking on attribute access
1class Obj:
2 def __init__(self, x):
3 self.b = x
4def get_obj():
5 return Obj(99)
6template = 'Result: %(obj)s'
7data = {'obj': get_obj().b, 'unused': None}
8try:
9 result = template % data
10 print('Output:', result)
11except Exception as e:
12 print('Error:', type(e).__name__, str(e))
Output: Result: 99
r2-p004 · Keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
1def process(*, fmt, **kw):
2 return fmt % kw
3def make_data():
4 return {'a': 1, 'extra': 2}
5template = '%(a)s'
6data = make_data()
7try:
8 result = process(fmt=template, **data)
9 print('Result:', result)
10except Exception as e:
11 print('Error:', type(e).__name__, str(e))
Result: 1
r2-p005 · Mixed formatting with tuple RHS - fix assumes mapping only
1template = '%(name)s %d'
2data = ({'name': 'Alice', 'extra': 99}, 42)
3try:
4 result = template % data
5 print('Output:', result)
6except Exception as e:
7 print('Error:', type(e).__name__, str(e))
Error: TypeError format requires a mapping
r2-p006 · File modes and truncation, partial writes, non-str data passed to write
1import os
2template = 'Log: %(msg)s'
3data = {'msg': 'hello', 'unused': open('tmp.txt', 'w')}
4try:
5 formatted = template % data
6 with open('out.txt', 'w') as f:
7 f.write(formatted)
8 with open('out.txt') as f:
9 print('File content:', repr(f.read()))
10except Exception as e:
11 print('Error:', type(e).__name__, str(e))
12finally:
13 if os.path.exists('tmp.txt'):
14 os.remove('tmp.txt')
15 if os.path.exists('out.txt'):
16 os.remove('out.txt')
File content: 'Log: hello'
r2-p007 · Walrus, comprehensions scoping, late binding in closures and lambdas
1funcs = []
2for i in range(2):
3 if (data := {'a': i, 'unused': i*10}) is not None:
4 template = '%(a)s'
5 f = lambda: template % data
6 funcs.append(f)
7print('Results:', [f() for f in funcs])
Results: ['1', '1']
r2-p008 · String formatting edge cases: %, format, f-strings, repr vs str, nested quotes
1name = 'World'
2extra = 42
3template = 'Hello %(name)s'
4data = {'name': name, 'extra': extra}
5try:
6 percent_result = template % data
7 format_result = template.format_map(data)
8 f_result = f'Hello {name}'
9 print('%:', percent_result)
10 print('.format_map:', format_result)
11 print('f-string:', f_result)
12except Exception as e:
13 print('Error:', type(e).__name__, str(e))
%: Hello World .format_map: Hello %(name)s f-string: Hello World