ruff B013
labelled unknown
Analysis Nemotron 3 Super
Flags single-element tuples in except clauses like 'except (ValueError,)'.
Fix: Rewrites 'except (SingleException,):' to 'except SingleException:' by removing the parentheses and trailing comma while preserving any 'as' alias, indentation, and following colon.
8 hazards
- Removing parentheses around a single exception type changes nothing semantically but may drop inline or end-of-line comments attached to the parentheses or comma, altering source code documentation.
- If the except clause spans multiple lines due to formatting, the fix may collapse vertical whitespace, potentially affecting readability but not execution.
- When the single element is a starred expression like '(*args,)', the rule correctly does not fire, so no rewrite occurs; however, misidentifying a starred expression as a regular tuple would lead to incorrect removal of parentheses, breaking unpacking semantics.
- In rare cases where the exception tuple is generated via a macro or code-generation tool that relies on the syntactic presence of a one-tuple for parsing, removing it could interfere with tooling (though this does not affect runtime behavior).
- If the except clause is part of a larger compound statement where parentheses were mistakenly believed to be necessary for disambiguation (e.g., confused with tuple displays in other contexts), removal is still safe because Python grammar unambiguously binds the except clause.
- The fix does not touch the 'as' target, so aliasing remains intact; however, if comments are interleaved between the exception and the 'as' keyword (e.g., 'except (ValueError, ) # note as e:'), the rewrite may disturb comment attachment though this is a formatting hazard, not semantic.
- Should the single element be an expression with side effects (e.g., a function call), evaluating it once versus as a one-tuple makes no difference, so no semantic change occurs.
- If the code is parsed by a tool that treats one-tuples distinctly in AST transformation (e.g., for refactoring), removing the tuple structure could alter the AST shape, potentially breaking downstream tools that expect a Tuple node with one element.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | invalid | Macro/code-generation dependency on one-tuple syntax | Code generation tools may inspect source syntax for one-tuples in except clauses; removing parentheses breaks their parsing even if runtime behavior is unchanged. |
| r1-p002 | fix ok | pathlib vs os.path behavior on relative paths, symlinks, missing files | Path resolution differences between pathlib and os.path when handling symlinks or missing files in except logic. |
| r1-p003 | fix ok | Multiline except clause collapsing vertical whitespace | When the except clause is formatted across multiple lines, the fix removes newlines and indentation, affecting readability though not execution. |
| r1-p004 | fix ok | Star-imports, module-level side effects, __all__ | Module-level side effects from star imports or __all__ manipulation interact with except flow; rewriting may obscure intent in complex modules. |
| r1-p005 | fix ok | Keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once | Default argument evaluation occurs at function definition; except logic may be involved in retry loops where default mutability matters. |
| r1-p006 | fix ok | Walrus, comprehensions scoping, late binding in closures and lambdas | Walrus in except or comprehension scoping interacts with lambda capture; rewriting may change when locals are bound in nested scopes. |
| r1-p007 | silent prediction off | AST transformation tools expecting Tuple node with one element | Tools that transform AST and rely on except clause being a Tuple node (even with one element) may break when it's changed to a Name node. |
| r1-p008 | fix ok | Exceptions inside rewritten code, finally/else clauses, exception chaining | Interaction with else/finally clauses or exception chains (__cause__) where rewriting affects control flow or nesting depth. |
| r2-p001 | fix ok | Compound statement ambiguity resolved correctly after fix | If the except clause is part of a larger compound statement where parentheses were mistakenly believed to be necessary for disambiguation (e.g., confused with tuple displays in other contexts), remova |
| r2-p002 | fix ok | Descriptor interaction with instance attribute access in handler | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p003 | fix ok | Bytes vs str encoding in error context | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p004 | fix ok | Keyword-only and positional-only argument forwarding in recovery | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p005 | fix ok | Inline comment stripped during parentheses removal alters documentation | Removing parentheses around a single exception type changes nothing semantically but may drop inline or end-of-line comments attached to the parentheses or comma, altering source code documentation. |
| r2-p006 | fix ok | File write mode and truncation behavior preserved after fix | file modes and truncation, partial writes, non-str data passed to write |
| r2-p007 | fix ok | Function call with side effects in except clause evaluated once | Should the single element be an expression with side effects (e.g., a function call), evaluating it once versus as a one-tuple makes no difference, so no semantic change occurs. |
| r2-p008 | fix ok | Numeric coercion and rounding behavior unchanged after fix | int/float/bool coercion, Decimal, division and rounding |
Sources
r1-p001 · Macro/code-generation dependency on one-tuple syntax (generated program does not parse: Traceback (most recent call last):)
1import ast
2
3def check_except_tuple():
4 src = '''\
5try:
6 raise ValueError('x')
7except (ValueError,) as e:\
8 return str(e)
9'''
10 tree = ast.parse(src)
11 handlers = [n for n in ast.walk(tree) if isinstance(n, ast.ExceptHandler)]
12 if handlers:
13 t = handlers[0].type
14 print('AST type:', type(t).__name__)
15 if isinstance(t, ast.Tuple):
16 print('Tuple elts:', len(t.elts))
17 else:
18 print('Not a tuple')
19 try:
20 raise ValueError('test')
21except (ValueError,) as e:
22 print('Caught:', e)
r1-p002 · pathlib vs os.path behavior on relative paths, symlinks, missing files
1import os
2from pathlib import Path
3
4Path('tmp').mkdir(exist_ok=True)
5os.symlink('target', 'tmp/link')
6
7try:
8 st = os.lstat('tmp/link') # Follows symlink by default? No, lstat doesn't.
9except (FileNotFoundError,) as e:
10 print('os.lstat caught:', type(e).__name__)
11
12try:
13 p = Path('tmp/link')
14 p.stat() # Follows symlink
15except (FileNotFoundError,) as e:
16 print('Path.stat caught:', type(e).__name__)
17
18# Cleanup
19os.unlink('tmp/link')
20os.rmdir('tmp')
--- orig.py 2026-10-17 16:13:36.449889456 -0400 +++ fixed.py 2026-10-17 16:13:36.636885870 -0400 @@ -6,13 +6,13 @@ try: st = os.lstat('tmp/link') # Follows symlink by default? No, lstat doesn't. -except (FileNotFoundError,) as e: +except FileNotFoundError as e: print('os.lstat caught:', type(e).__name__) try: p = Path('tmp/link') p.stat() # Follows symlink -except (FileNotFoundError,) as e: +except FileNotFoundError as e: print('Path.stat caught:', type(e).__name__) # Cleanup
Path.stat caught: FileNotFoundError
r1-p003 · Multiline except clause collapsing vertical whitespace
1try:
2 raise RuntimeError('fail')
3except (
4 RuntimeError,
5) as e:
6 print('Caught:', e)
7 print('Type:', type(e))
--- orig.py 2026-10-17 16:13:36.747883742 -0400 +++ fixed.py 2026-10-17 16:13:36.896880885 -0400 @@ -1,7 +1,5 @@ try: raise RuntimeError('fail') -except ( - RuntimeError, -) as e: +except RuntimeError as e: print('Caught:', e) print('Type:', type(e))
Caught: fail Type: <class 'RuntimeError'>
r1-p004 · Star-imports, module-level side effects, __all__
1__all__ = ['CustomError']
2
3class CustomError(Exception):
4 pass
5
6def func():
7 try:
8 raise CustomError('module side effect')
9 except (CustomError,) as e:
10 print('Caught in func:', e)
11 return len(__all__)
12
13print(func())
--- orig.py 2026-10-17 16:13:36.999878910 -0400 +++ fixed.py 2026-10-17 16:13:37.159875849 -0400 @@ -6,7 +6,7 @@ def func(): try: raise CustomError('module side effect') - except (CustomError,) as e: + except CustomError as e: print('Caught in func:', e) return len(__all__)
Caught in func: module side effect 1
r1-p005 · Keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
1def f(x, y=[]):
2 try:
3 if not y:
4 raise ValueError('empty')
5 except (ValueError,) as e:
6 y.append(x)
7 return y
8 return y
9
10print(f(1))
11print(f(2))
12print(f(3)) # Default list accumulates across calls
--- orig.py 2026-10-17 16:13:37.257873974 -0400 +++ fixed.py 2026-10-17 16:13:37.419870875 -0400 @@ -2,7 +2,7 @@ try: if not y: raise ValueError('empty') - except (ValueError,) as e: + except ValueError as e: y.append(x) return y return y
[1] [1] [1]
r1-p006 · Walrus, comprehensions scoping, late binding in closures and lambdas
1funs = []
2for i in range(3):
3 try:
4 if (val := i) == 1:
5 raise KeyError('one')
6 except (KeyError,) as e:
7 funs.append(lambda: val + i) # Captures i and val
8
9for f in funs:
10 print(f())
--- orig.py 2026-10-17 16:13:37.514869058 -0400 +++ fixed.py 2026-10-17 16:13:37.681865863 -0400 @@ -3,7 +3,7 @@ try: if (val := i) == 1: raise KeyError('one') - except (KeyError,) as e: + except KeyError as e: funs.append(lambda: val + i) # Captures i and val for f in funs:
4
r1-p007 · AST transformation tools expecting Tuple node with one element
1import ast
2
3def transform(node):
4 for n in ast.walk(node):
5 if isinstance(n, ast.ExceptHandler) and isinstance(n.type, ast.Tuple):
6 print(f'Found except tuple with {len(n.type.elts)} element(s)')
7 return
8 print('No tuple-type except handler found')
9
10src = '''
11try:
12 raise ValueError
13except (ValueError,) as e:
14 pass
15'''
16tree = ast.parse(src)
17transform(tree)
Found except tuple with 1 element(s)
r1-p008 · Exceptions inside rewritten code, finally/else clauses, exception chaining
1try:
2 try:
3 raise ValueError('first')
4 except (ValueError,) as e:
5 raise RuntimeError('second') from e
6except (RuntimeError,) as e:
7 print('Caught:', e)
8 print('Cause:', type(e.__cause__).__name__ if e.__cause__ else None)
9else:
10 print('No exception')
11finally:
12 print('Finally ran')
--- orig.py 2026-10-17 16:13:37.933861043 -0400 +++ fixed.py 2026-10-17 16:13:38.095857948 -0400 @@ -1,9 +1,9 @@ try: try: raise ValueError('first') - except (ValueError,) as e: + except ValueError as e: raise RuntimeError('second') from e -except (RuntimeError,) as e: +except RuntimeError as e: print('Caught:', e) print('Cause:', type(e.__cause__).__name__ if e.__cause__ else None) else:
Caught: second Cause: ValueError Finally ran
r2-p001 · Compound statement ambiguity resolved correctly after fix
1def f():
2 try:
3 raise ValueError
4 except (ValueError,):
5 return 42
6print(f())
--- orig.py 2026-10-17 16:13:45.683714277 -0400 +++ fixed.py 2026-10-17 16:13:45.846711217 -0400 @@ -1,6 +1,6 @@ def f(): try: raise ValueError - except (ValueError,): + except ValueError: return 42 print(f())
42
r2-p002 · Descriptor interaction with instance attribute access in handler
1class D:
2 def __get__(self, obj, objtype=None):
3 return 'descriptor'
4class C:
5 x = D()
6c = C()
7try:
8 c.x # triggers descriptor
9 raise KeyError
10except (KeyError,):
11 print(c.x)
--- orig.py 2026-10-17 16:13:45.946709339 -0400 +++ fixed.py 2026-10-17 16:13:46.110706266 -0400 @@ -7,5 +7,5 @@ try: c.x # triggers descriptor raise KeyError -except (KeyError,): +except KeyError: print(c.x)
descriptor
r2-p003 · Bytes vs str encoding in error context
1try:
2 b'\xff'.decode('utf-8')
3except (UnicodeDecodeError,) as e:
4 print(type(e).__name__, len(str(e)))
5try:
6 ''.encode('utf-8').decode('latin-1')
7except (UnicodeEncodeError,) as e:
8 print(type(e).__name__, len(str(e)))
--- orig.py 2026-10-17 16:13:46.208704430 -0400 +++ fixed.py 2026-10-17 16:13:46.362701546 -0400 @@ -1,8 +1,8 @@ try: b'\xff'.decode('utf-8') -except (UnicodeDecodeError,) as e: +except UnicodeDecodeError as e: print(type(e).__name__, len(str(e))) try: ''.encode('utf-8').decode('latin-1') -except (UnicodeEncodeError,) as e: +except UnicodeEncodeError as e: print(type(e).__name__, len(str(e)))
UnicodeDecodeError 70
r2-p004 · Keyword-only and positional-only argument forwarding in recovery
1def f(a, /, b, *, c=1):
2 return a + b + c
3try:
4 f(1, 2, c=3) # works
5 raise TypeError
6except (TypeError,) as e:
7 print(f(4, 5, c=6))
--- orig.py 2026-10-17 16:13:46.456699785 -0400 +++ fixed.py 2026-10-17 16:13:46.631696507 -0400 @@ -3,5 +3,5 @@ try: f(1, 2, c=3) # works raise TypeError -except (TypeError,) as e: +except TypeError as e: print(f(4, 5, c=6))
15
r2-p005 · Inline comment stripped during parentheses removal alters documentation
1try:
2 1 / 0
3except (ZeroDivisionError,) as e: # handle division by zero
4 print('caught:', type(e).__name__)
--- orig.py 2026-10-17 16:13:46.726694728 -0400 +++ fixed.py 2026-10-17 16:13:46.881691824 -0400 @@ -1,4 +1,4 @@ try: 1 / 0 -except (ZeroDivisionError,) as e: # handle division by zero +except ZeroDivisionError as e: # handle division by zero print('caught:', type(e).__name__)
caught: ZeroDivisionError
r2-p006 · File write mode and truncation behavior preserved after fix
1import os
2try:
3 with open('test.tmp', 'wb') as f:
4 f.write(b'hello')
5 raise OSError
6except (OSError,) as e:
7 with open('test.tmp', 'rb') as f:
8 print(f.read())
9finally:
10 if os.path.exists('test.tmp'):
11 os.remove('test.tmp')
--- orig.py 2026-10-17 16:13:46.983689914 -0400 +++ fixed.py 2026-10-17 16:13:47.150686793 -0400 @@ -3,7 +3,7 @@ with open('test.tmp', 'wb') as f: f.write(b'hello') raise OSError -except (OSError,) as e: +except OSError as e: with open('test.tmp', 'rb') as f: print(f.read()) finally:
b'hello'
r2-p007 · Function call with side effects in except clause evaluated once
1state = 0
2
3def get_exc():
4 global state
5 state += 1
6 return ValueError
7
8try:
9 raise get_exc()
10except (get_exc(),) as e:
11 print('state:', state, 'exc:', type(e).__name__)
--- orig.py 2026-10-17 16:13:47.243685055 -0400 +++ fixed.py 2026-10-17 16:13:47.397682177 -0400 @@ -7,5 +7,5 @@ try: raise get_exc() -except (get_exc(),) as e: +except get_exc() as e: print('state:', state, 'exc:', type(e).__name__)
state: 2 exc: ValueError
r2-p008 · Numeric coercion and rounding behavior unchanged after fix
1from decimal import Decimal, getcontext
2getcontext().prec = 2
3try:
4 x = Decimal('1') / Decimal('3')
5 if x > Decimal('0.33'):
6 raise TypeError
7except (TypeError,) as e:
8 y = float(Decimal('2.5'))
9 print(int(y), bool(y))
--- orig.py 2026-10-17 16:13:47.496680327 -0400 +++ fixed.py 2026-10-17 16:13:47.684676815 -0400 @@ -4,6 +4,6 @@ x = Decimal('1') / Decimal('3') if x > Decimal('0.33'): raise TypeError -except (TypeError,) as e: +except TypeError as e: y = float(Decimal('2.5')) print(int(y), bool(y))