Description
The default Python extractor (the blib2to3-based parser, not the tree-sitter one) extracts a PEP 758 unparenthesized two-type except clause using the Python 2 reading: the second exception type is recorded as an alias binding (Store) rather than as a use (Load).
Any query that reasons about whether a name is used then misfires. The one I hit is py/unused-import.
PEP 758 is Final and shipped in Python 3.14, and ruff format actively rewrites except (A, B): to except A, B: for projects targeting py314, so this is reachable in ordinary formatted code.
Reproduction
Verified with CodeQL 2.26.3 (codeql-bundle-v2.26.3, the current bundle) on macOS.
errs.py:
class Alpha(Exception):
pass
class Beta(Exception):
pass
unparenthesized.py:
from errs import Alpha, Beta
def f():
try:
pass
except Alpha, Beta:
raise
parenthesized.py — identical but except (Alpha, Beta):.
codeql database create db --language=python --source-root=src
codeql database analyze db --format=csv --output=res.csv \
--rerun codeql/python-queries:Imports/UnusedImport.ql
Result:
"Unused import",...,"Import of 'Beta' is not used.","/unparenthesized.py","1","1","1","28"
parenthesized.py is clean. The only difference between the two files is the parentheses.
Root cause
Probing the database shows the contexts differ:
| file |
line 7 |
Alpha |
Beta |
parenthesized.py |
except (Alpha, Beta): |
Load |
Load |
unparenthesized.py |
except Alpha, Beta: |
Load |
Store |
and the AST shape confirms the Python 2 reading:
| file |
ExceptStmt.getType() |
ExceptStmt.getName() |
unparenthesized.py:7 |
Alpha (a Name) |
Beta |
parenthesized.py:7 |
Tuple |
none |
The grammar rule in blib2to3/Grammar.txt is shared between the two readings:
except_clause: 'except' [test [(',' | 'as') test]]
and semmle/python/parser/ast.py::visit_except_clause never inspects the separator token:
def visit_except_clause(self, node):
type, name = None, None
if len(node.children) > 1:
type = self.visit(node.children[1], LOAD)
if len(node.children) > 3:
name = self.visit(node.children[3], STORE) # assumes children[2] is 'as'
return type, name
So whenever there are four children, the fourth is bound as an alias regardless of whether the separator was , or as.
Scope
Only the two-type unparenthesized form is affected. except A, B, C: extracts correctly (three types, no alias), presumably because it fails this rule and reaches a different parse. Parenthesized forms are unaffected.
Note on the tree-sitter parser
#20990 added PEP 758 support to the tree-sitter parser, and python/extractor/tests/parser/exceptions_new.expected records the correct AST (both names Load, name: None). That parser is not the default, so the fix did not reach analysis. The two parsers currently disagree on this input, which the unsuffixed parser tests are meant to prevent.
Fix
PR #22386 proposes checking the separator token, so , builds a tuple of exception types and only as binds an alias. With it:
- the default parser reproduces the existing
exceptions_new.expected byte for byte;
- of the 37 files in
tests/parser/, only the two containing PEP 758 syntax change at all;
- patching the fix into the 2.26.3 bundle makes the false positive above disappear, while a genuinely unused import in the same file is still correctly reported.
There is a trade-off worth calling out: Python 2 source of the form except ValueError, e: that happens to parse under the Python 3 grammar will now be read as a tuple of types rather than an alias binding. The tree-sitter parser already made that choice, so this aligns the two.
Investigated and written with assistance from Claude Code; the reproduction, the database probes, and the test results above were all executed against the real 2.26.3 bundle rather than inferred.
Description
The default Python extractor (the
blib2to3-based parser, not the tree-sitter one) extracts a PEP 758 unparenthesized two-typeexceptclause using the Python 2 reading: the second exception type is recorded as an alias binding (Store) rather than as a use (Load).Any query that reasons about whether a name is used then misfires. The one I hit is
py/unused-import.PEP 758 is Final and shipped in Python 3.14, and
ruff formatactively rewritesexcept (A, B):toexcept A, B:for projects targetingpy314, so this is reachable in ordinary formatted code.Reproduction
Verified with CodeQL 2.26.3 (
codeql-bundle-v2.26.3, the current bundle) on macOS.errs.py:unparenthesized.py:parenthesized.py— identical butexcept (Alpha, Beta):.Result:
parenthesized.pyis clean. The only difference between the two files is the parentheses.Root cause
Probing the database shows the contexts differ:
AlphaBetaparenthesized.pyexcept (Alpha, Beta):unparenthesized.pyexcept Alpha, Beta:and the AST shape confirms the Python 2 reading:
ExceptStmt.getType()ExceptStmt.getName()unparenthesized.py:7Alpha(aName)Betaparenthesized.py:7TupleThe grammar rule in
blib2to3/Grammar.txtis shared between the two readings:and
semmle/python/parser/ast.py::visit_except_clausenever inspects the separator token:So whenever there are four children, the fourth is bound as an alias regardless of whether the separator was
,oras.Scope
Only the two-type unparenthesized form is affected.
except A, B, C:extracts correctly (three types, no alias), presumably because it fails this rule and reaches a different parse. Parenthesized forms are unaffected.Note on the tree-sitter parser
#20990 added PEP 758 support to the tree-sitter parser, and
python/extractor/tests/parser/exceptions_new.expectedrecords the correct AST (both namesLoad,name: None). That parser is not the default, so the fix did not reach analysis. The two parsers currently disagree on this input, which the unsuffixed parser tests are meant to prevent.Fix
PR #22386 proposes checking the separator token, so
,builds a tuple of exception types and onlyasbinds an alias. With it:exceptions_new.expectedbyte for byte;tests/parser/, only the two containing PEP 758 syntax change at all;There is a trade-off worth calling out: Python 2 source of the form
except ValueError, e:that happens to parse under the Python 3 grammar will now be read as a tuple of types rather than an alias binding. The tree-sitter parser already made that choice, so this aligns the two.Investigated and written with assistance from Claude Code; the reproduction, the database probes, and the test results above were all executed against the real 2.26.3 bundle rather than inferred.