fixhunt

ruff B013

labelled unknown
Runs / 2026-10-17T20-13-12-213Z-d042-ruff-B013ruff 0.17.12026-10-17 20:13 UTC8 programs × 2 round(s)16 programs, 14 fired, 14 fixed, 0 findings, 1 rejectedmodel cost $0.0049

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

idresulttitlehazard
r1-p001invalidMacro/code-generation dependency on one-tuple syntaxCode generation tools may inspect source syntax for one-tuples in except clauses; removing parentheses breaks their parsing even if runtime behavior is unchanged.
r1-p002fix okpathlib vs os.path behavior on relative paths, symlinks, missing filesPath resolution differences between pathlib and os.path when handling symlinks or missing files in except logic.
r1-p003fix okMultiline except clause collapsing vertical whitespaceWhen the except clause is formatted across multiple lines, the fix removes newlines and indentation, affecting readability though not execution.
r1-p004fix okStar-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-p005fix okKeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated onceDefault argument evaluation occurs at function definition; except logic may be involved in retry loops where default mutability matters.
r1-p006fix okWalrus, comprehensions scoping, late binding in closures and lambdasWalrus in except or comprehension scoping interacts with lambda capture; rewriting may change when locals are bound in nested scopes.
r1-p007silent prediction offAST transformation tools expecting Tuple node with one elementTools 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-p008fix okExceptions inside rewritten code, finally/else clauses, exception chainingInteraction with else/finally clauses or exception chains (__cause__) where rewriting affects control flow or nesting depth.
r2-p001fix okCompound statement ambiguity resolved correctly after fixIf 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-p002fix okDescriptor interaction with instance attribute access in handlerclass attributes vs instance attributes, descriptors, properties with side effects
r2-p003fix okBytes vs str encoding in error contextbytes vs str, encoding and newline handling, text vs binary modes
r2-p004fix okKeyword-only and positional-only argument forwarding in recoverykeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p005fix okInline comment stripped during parentheses removal alters documentationRemoving 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-p006fix okFile write mode and truncation behavior preserved after fixfile modes and truncation, partial writes, non-str data passed to write
r2-p007fix okFunction call with side effects in except clause evaluated onceShould 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-p008fix okNumeric coercion and rounding behavior unchanged after fixint/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))