Non-standard exception raised in special method¶
ID: py/unexpected-raise-in-special-method
Kind: problem
Security severity:
Severity: recommendation
Precision: high
Tags:
- quality
- reliability
- error-handling
Query suites:
- python-security-and-quality.qls
Click to see the query in the CodeQL repository
User-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 __add__ and __radd__ special methods. When the expression a + b is evaluated, the Python virtual machine will call type(a).__add__(a, b), and if that is not implemented it will call type(b).__radd__(b, a).
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 a.b may raise an AttributeError if the object a does not have an attribute b. If a KeyError were raised instead, then this would be unexpected and may break code that expected an AttributeError, but not a KeyError.
Therefore, if a method is unable to perform the expected operation then its response should conform to the standard protocol, described below.
Attribute access,
a.b(__getattr__): RaiseAttributeError.Arithmetic operations,
a + b(__add__): Do not raise an exception, returnNotImplementedinstead.Indexing,
a[b](__getitem__): RaiseKeyErrororIndexError.Hashing,
hash(a)(__hash__): Should not raise an exception. Use__hash__ = Noneto indicate that an object is unhashable rather than raising an exception.Equality methods,
a == b(__eq__): Never raise an exception, always returnTrueorFalse.Ordering comparison methods,
a < b(__lt__): Raise aTypeErrorif the objects cannot be ordered.Most others: If the operation is never supported, the method often does not need to be implemented at all; otherwise a
TypeErrorshould be raised.
Recommendation¶
If the method always raises as exception, then if it is intended to be an abstract method, the @abstractmethod decorator should be used. Otherwise, ensure that the method raises an exception of the correct type, or remove the method if the operation does not need to be supported.
Example¶
In the following example, the __add__ method of A raises a TypeError if other is of the wrong type. However, it should return NotImplemented instead of rising an exception, to allow other classes to support adding to A. This is demonstrated in the class B.
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 __getitem__ method of C raises a ValueError, rather than a KeyError or IndexError as expected.
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 __hash__ method of D raises NotImplementedError. This causes D to be incorrectly identified as hashable by isinstance(obj, collections.abc.Hashable); so the correct way to make a class unhashable is to set __hash__ = None.
class D:
def __hash__(self):
# BAD: Use `__hash__ = None` instead.
raise NotImplementedError(f"{self.__class__} is unhashable.")
References¶
Python Language Reference: Special Method Names.
Python Library Reference: Exceptions.