ruff FURB156
labelled unknown
Analysis Nemotron 3 Super
Flags hardcoded strings matching known charset literals like digits or ascii letters and suggests replacing them with corresponding `string` module constants.
Fix: Replaces the hardcoded string literal with an attribute access on the `string` module (e.g., `string.digits`, `string.ascii_lowercase`) and adds `import string` at the top of the file if not already present.
8 hazards
- If the string literal appears inside a function or class scope, adding `import string` at the module level may cause unnecessary imports or circular import issues in rare cases.
- If the string is used as a default argument value, the rewrite changes the timing of when the `string` module is accessed (at definition time vs call time), potentially affecting behavior if the module is modified later.
- If the hardcoded string is part of a larger expression (e.g., concatenation or formatting), replacing only the literal may break the expression or produce a syntax error.
- If the variable name `string` is already in use in the current scope, the import will shadow it, causing `string.digits` to fail with an AttributeError or refer to the wrong object.
- If the code relies on the specific identity of the string literal (e.g., for interning or `is` comparisons), the rewrite may produce a different string object, breaking identity checks.
- If the hardcoded string contains escaped characters (like `\t\n\v\f\r `) and is edited manually, the autofix might not recognize it as whitespace, leading to incomplete fixes.
- If the string is used in a context where its exact byte representation matters (e.g., encoding, cryptography, or binary protocols), replacing it with a `string` constant could alter behavior if the constant differs in encoding or composition.
- If the code is generated or templated and the string appears in a comment or docstring, the tool might incorrectly modify it, though the rule should avoid non-code contexts—however, flawed parsing could still lead to erroneous changes.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | String Literal Identity Check | If the code relies on the specific identity of the string literal (e.g., for interning or `is` comparisons), the rewrite may produce a different string object, breaking identity checks. |
| r1-p002 | fix ok | Class Attribute Descriptor Side Effect | class attributes vs instance attributes, descriptors, properties with side effects |
| r1-p003 | fix ok | Function Scope Import Causing Circular Risk | If the string literal appears inside a function or class scope, adding `import string` at the module level may cause unnecessary imports or circular import issues in rare cases. |
| r1-p004 | fix ok | Comment Attached to Removed Literal | comments and docstrings attached to the removed or moved code |
| r1-p005 | silent prediction off | Default Argument Evaluation Once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p006 | fix ok | Generator Consumption Laziness | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r1-p007 | silent | Generated Code in Comment Misidentified | If the code is generated or templated and the string appears in a comment or docstring, the tool might incorrectly modify it, though the rule should avoid non-code contexts—however, flawed parsing cou |
| r1-p008 | fix ok | Exception Handling with Finally | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p001 | fix ok | Exception handling with finally and else clauses | Exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p002 | fix ok | Function scope import causing circular risk | If the string literal appears inside a function or class scope, adding `import string` at the module level may cause unnecessary imports or circular import issues in rare cases. |
| r2-p003 | silent prediction off | Star-imports and module-level side effects | star-imports, module-level side effects, __all__ |
| r2-p004 | silent prediction off | Escaped whitespace in hardcoded string | If the hardcoded string contains escaped characters (like `\t\n\v\f\r `) and is edited manually, the autofix might not recognize it as whitespace, leading to incomplete fixes. |
| r2-p005 | silent prediction off | Variable name `string` already in use | If the variable name `string` is already in use in the current scope, the import will shadow it, causing `string.digits` to fail with an AttributeError or refer to the wrong object. |
| r2-p006 | fix ok | Bytes vs str, encoding and newline handling | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p007 | fix ok | File modes and truncation, partial writes | file modes and truncation, partial writes, non-str data passed to write |
| r2-p008 | fix ok | Evaluation order and side effects in merged expressions | evaluation order and side effects when expressions are merged, duplicated or deleted |
Sources
r1-p001 · String Literal Identity Check
1s = "0123456789"
2t = "0123456789"
3print(s is t)
4s_replaced = "0123456789" # This line triggers FURB156
5t_replaced = "0123456789"
6print(s_replaced is t_replaced)
--- orig.py 2026-10-17 17:33:28.992365465 -0400 +++ fixed.py 2026-10-17 17:33:29.156363352 -0400 @@ -1,6 +1,7 @@ -s = "0123456789" -t = "0123456789" +import string +s = string.digits +t = string.digits print(s is t) -s_replaced = "0123456789" # This line triggers FURB156 -t_replaced = "0123456789" +s_replaced = string.digits # This line triggers FURB156 +t_replaced = string.digits print(s_replaced is t_replaced)
True True
r1-p002 · Class Attribute Descriptor Side Effect
1class Meta(type):
2 def __setattr__(cls, name, value):
3 print(f"Setting {name}={value}")
4 super().__setattr__(name, value)
5
6class C(metaclass=Meta):
7 digits = "0123456789" # Triggers FURB156
8
9print(C.digits)
--- orig.py 2026-10-17 17:33:29.253362103 -0400 +++ fixed.py 2026-10-17 17:33:29.416360003 -0400 @@ -1,9 +1,10 @@ +import string class Meta(type): def __setattr__(cls, name, value): print(f"Setting {name}={value}") super().__setattr__(name, value) class C(metaclass=Meta): - digits = "0123456789" # Triggers FURB156 + digits = string.digits # Triggers FURB156 print(C.digits)
0123456789
r1-p003 · Function Scope Import Causing Circular Risk
1def f():
2 try:
3 import string # Local import to avoid circular
4 except ImportError:
5 pass
6 if '0' in "0123456789": # Triggers FURB156
7 return "found"
8 return "not found"
9
10print(f())
--- orig.py 2026-10-17 17:33:29.515358728 -0400 +++ fixed.py 2026-10-17 17:33:29.683356564 -0400 @@ -3,7 +3,7 @@ import string # Local import to avoid circular except ImportError: pass - if '0' in "0123456789": # Triggers FURB156 + if '0' in string.digits: # Triggers FURB156 return "found" return "not found"
found
r1-p004 · Comment Attached to Removed Literal
1# This comment describes the digit set
2DIGITS = "0123456789" # Triggers FURB156 - used for validation
3print(DIGITS)
--- orig.py 2026-10-17 17:33:29.783355276 -0400 +++ fixed.py 2026-10-17 17:33:29.944353202 -0400 @@ -1,3 +1,4 @@ # This comment describes the digit set -DIGITS = "0123456789" # Triggers FURB156 - used for validation +import string +DIGITS = string.digits # Triggers FURB156 - used for validation print(DIGITS)
0123456789
r1-p005 · Default Argument Evaluation Once
1def process(c=[]):
2 if 'a' in "0123456789abcdefghijklmnopqrstuvwxyz": # Triggers FURB156
3 c.append('matched')
4 return c
5
6print(process())
7print(process())
8print(process([]))
['matched'] ['matched', 'matched'] ['matched']
r1-p006 · Generator Consumption Laziness
1def char_gen():
2 for ch in "abcdefghijklmnopqrstuvwxyz": # Triggers FURB156
3 yield ch
4
5g = char_gen()
6print(list(g))
7print(list(g)) # Should be empty if exhausted
--- orig.py 2026-10-17 17:33:30.176350216 -0400 +++ fixed.py 2026-10-17 17:33:30.349347991 -0400 @@ -1,5 +1,6 @@ +import string def char_gen(): - for ch in "abcdefghijklmnopqrstuvwxyz": # Triggers FURB156 + for ch in string.ascii_lowercase: # Triggers FURB156 yield ch g = char_gen()
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] []
r1-p007 · Generated Code in Comment Misidentified
1## This is a generated template
2## digits = "0123456789" # Should NOT be touched
3## But if flawed parsing occurs, it might break
4print("safe")
safe
r1-p008 · Exception Handling with Finally
1try:
2 if 'x' in "abcdefghijklmnopqrstuvwxyz": # Triggers FURB156
3 raise ValueError("found x")
4except ValueError as e:
5 print(f"Caught: {e}")
6finally:
7 print("Finally block executed")
--- orig.py 2026-10-17 17:33:30.584344968 -0400 +++ fixed.py 2026-10-17 17:33:30.747342871 -0400 @@ -1,5 +1,6 @@ +import string try: - if 'x' in "abcdefghijklmnopqrstuvwxyz": # Triggers FURB156 + if 'x' in string.ascii_lowercase: # Triggers FURB156 raise ValueError("found x") except ValueError as e: print(f"Caught: {e}")
Caught: found x Finally block executed
r2-p001 · Exception handling with finally and else clauses
1try:
2 if 'x' in '0123456789':
3 raise ValueError('found')
4except ValueError as e:
5 print('caught:', e)
6else:
7 print('no error')
8finally:
9 print('cleanup')
--- orig.py 2026-10-17 17:33:35.099287057 -0400 +++ fixed.py 2026-10-17 17:33:35.266284923 -0400 @@ -1,5 +1,6 @@ +import string try: - if 'x' in '0123456789': + if 'x' in string.digits: raise ValueError('found') except ValueError as e: print('caught:', e)
no error cleanup
r2-p002 · Function scope import causing circular risk
1def f():
2 return 'a' in '0123456789'
3print(f())
--- orig.py 2026-10-17 17:33:35.367283632 -0400 +++ fixed.py 2026-10-17 17:33:35.526281599 -0400 @@ -1,3 +1,4 @@ +import string def f(): - return 'a' in '0123456789' + return 'a' in string.digits print(f())
False
r2-p003 · Star-imports and module-level side effects
1from string import *
2print(digits)
3# Rule should not fire here because 'digits' is already imported via star
4# But if it did, it would break due to name conflict after fix attempt
0123456789
r2-p004 · Escaped whitespace in hardcoded string
1s = '\t\n\v\f\r '
2if '\t' in s:
3 print('tab found')
4else:
5 print('tab not found')
tab found
r2-p005 · Variable name `string` already in use
1string = 'overridden'
2try:
3 print(string.upper())
4except AttributeError as e:
5 print('Error:', e)
6# After fix: import string would shadow local var, but we simulate risk by showing what happens if name clashes
OVERRIDDEN
r2-p006 · Bytes vs str, encoding and newline handling
1data = b'0123456789'
2if b'5' in data:
3 print('byte found')
4text = '0123456789'
5if '5' in text:
6 print('char found')
--- orig.py 2026-10-17 17:33:36.009275426 -0400 +++ fixed.py 2026-10-17 17:33:36.179273256 -0400 @@ -1,6 +1,7 @@ +import string data = b'0123456789' if b'5' in data: print('byte found') -text = '0123456789' +text = string.digits if '5' in text: print('char found')
byte found char found
r2-p007 · File modes and truncation, partial writes
1with open('test.txt', 'w') as f:
2 f.write('0123456789')
3with open('test.txt', 'r') as f:
4 content = f.read()
5 print('read:', repr(content))
6import os
7os.remove('test.txt')
--- orig.py 2026-10-17 17:33:36.277272005 -0400 +++ fixed.py 2026-10-17 17:33:36.437269962 -0400 @@ -1,5 +1,6 @@ +import string with open('test.txt', 'w') as f: - f.write('0123456789') + f.write(string.digits) with open('test.txt', 'r') as f: content = f.read() print('read:', repr(content))
read: '0123456789'
r2-p008 · Evaluation order and side effects in merged expressions
1side = []
2def get_digit_str():
3 side.append('called')
4 return '0123456789'
5if '5' in get_digit_str():
6 print('side effect log:', side)
--- orig.py 2026-10-17 17:33:36.535268711 -0400 +++ fixed.py 2026-10-17 17:33:36.702266580 -0400 @@ -1,6 +1,7 @@ +import string side = [] def get_digit_str(): side.append('called') - return '0123456789' + return string.digits if '5' in get_digit_str(): print('side effect log:', side)
side effect log: ['called']