-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Python: Modernize Unexpected Raise In Special Method query #20120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Python: Modernize Unexpected Raise In Special Method query #20120
Conversation
…e the exception is not a simple identifier.
QHelp previews: python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelpNon-standard exception raised in special methodUser-defined classes interact with the Python virtual machine via special methods (also called "magic methods"). For example, for a class to support addition it must implement the Since the virtual machine calls these special methods for common expressions, users of the class will expect these operations to raise standard exceptions. For example, users would expect that the expression Therefore, if a method is unable to perform the expected operation then its response should conform to the standard protocol, described below.
RecommendationIf the method is intended to be abstract, and thus always raise an exception, then declare it so using the ExampleIn the following example, the class A:
def __init__(self, a):
self.a = a
def __add__(self, other):
# BAD: Should return NotImplemented instead of raising
if not isinstance(other,A):
raise TypeError(f"Cannot add A to {other.__class__}")
return A(self.a + other.a)
class B:
def __init__(self, a):
self.a = a
def __add__(self, other):
# GOOD: Returning NotImplemented allows for the operation to fallback to other implementations to allow other classes to support adding to B.
if not isinstance(other,B):
return NotImplemented
return B(self.a + other.a)
In the following example, the class C:
def __getitem__(self, idx):
if self.idx < 0:
# BAD: Should raise a KeyError or IndexError instead.
raise ValueError("Invalid index")
return self.lookup(idx)
In the following example, the class class D:
def __hash__(self):
# BAD: Use `__hash__ = None` instead.
raise NotImplementedError(f"{self.__class__} is unhashable.") References
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR modernizes the py/unexpected-raise-in-special-method
CodeQL query by replacing the pointsTo
analysis with more modern dataflow techniques, enabling detection of conditionally raised exceptions, and reducing false positives. The precision has been changed from very-high
to high
to reflect the expanded scope.
- Replaces the legacy
pointsTo
analysis with modern API graphs and dataflow analysis - Extends detection to cases where exceptions are raised conditionally, not just always
- Improves the accuracy of exception type detection and reduces false positives
Reviewed Changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql |
Complete rewrite using modern CodeQL libraries and expanded logic for conditional exception detection |
python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py |
New comprehensive test file with various special method scenarios |
python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod*.py |
Updated example files demonstrating correct and incorrect patterns |
python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp |
Updated documentation with clearer explanations and new examples |
python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md |
Release notes documenting the changes |
Comments suppressed due to low confidence (1)
python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py:3
- The variable
self.idx
should likely beidx
(the parameter). Usingself.idx
assumes the object has anidx
attribute, which is not defined in this class.
def __add__(self, other): # No alert - Always allow NotImplementedError
In addition to replacing
pointsTo
, the query functionality has been altered in a relatively substantial way:Previously, the query only ever alerts cases in which a exception is always raised; and thus would only alert for "stub" methods raising things ike TypeError or NotImplementedError, for unsopported operations.
However, parts of the query documentation and parts of the query implementation implied that it should be checking for exceptions that are conditionally raised, rather than only exceptions that are always raised (such as which errors are checked - i.e. it would rarely if ever make sense to always return an ArithmeticError from an arithmetic operation like
__add__
, but that is what the query checked for and recomends.)Thus, I have decided to enable the checks for conditionally raised exceptions as well.
This yeilds more results; some MRVA testing look like likly TPs, as well as others which are possible FPs.
A few FP cases have been adressed in the query (e.g. removing checks for methods like
__setitem__
, as while it should raise a KeyError or IndexError if an error is to do with the 2nd (key) argument not being found, it makes sense to allow for raising other types of exception depending on the 3rd (value) argument.The precision has also been lowered from very-high to high, to reflect that the FP rate may be higher, though the overall result count including TPs is also higher.