__matmul__ / __rmatmul__
Overview
Python’s @ operator performs matrix multiplication. It was introduced in Python 3.5 via PEP 465 specifically to support NumPy’s matrix operations, though you can implement it for any custom type.
Three dunder methods control how @ behaves:
| Method | Operator | When called |
|---|---|---|
__matmul__ | a @ b | Python calls a.__matmul__(b) first |
__rmatmul__ | a @ b | Python calls b.__rmatmul__(a) if a.__matmul__ returns NotImplemented |
__imatmul__ | a @= b | Python calls a.__imatmul__(b) for in-place augmented assignment |
__matmul__(self, other)
Implements the left-hand side of the @ operator.
def __matmul__(self, other) -> Any
Parameters
self— the left operandother— the right operand (typically a matrix, vector, or compatible object)
Return value
Return the result of the matrix multiplication, or NotImplemented if the operation is not supported for the given types.
__rmatmul__(self, other)
Implements the right-hand side of the @ operator. Python calls this when the left operand does not implement __matmul__ or returns NotImplemented.
def __rmatmul__(self, other) -> Any
Parameters
self— the right operand (this object)other— the left operand
Return value
Return the result of the reversed matrix multiplication, or NotImplemented to let Python continue its fallback chain.
__imatmul__(self, other)
Implements the in-place @= augmented assignment operator.
def __imatmul__(self, other) -> Any
Parameters
self— the target object (left operand)other— the right operand
Return value
Must return self after mutating in place. This is required for augmented assignment operators to work correctly.
A gotcha with @=
Despite the @= syntax suggesting in-place mutation, Python treats a @= b as a = a @ b for most objects. NumPy arrays specifically do not mutate in-place with @= — they rebind the variable to a new array. The __imatmul__ method exists, but whether it actually produces in-place behavior depends on the implementation.
Operator Resolution
When Python evaluates a @ b:
- Calls
a.__matmul__(b) - If that returns
NotImplemented, callsb.__rmatmul__(a) - If both return
NotImplemented, raisesTypeError
Returning NotImplemented (rather than raising TypeError) allows Python’s fallback chain to try the reverse operation. This matters when mixing types that each implement partial support for @.
Practical Example
A Matrix class that implements all three methods:
class Matrix:
def __init__(self, data):
self.data = data
def __matmul__(self, other):
if not isinstance(other, Matrix):
return NotImplemented
if len(self.data[0]) != len(other.data):
raise ValueError(
f"Dimension mismatch: {len(self.data[0])} != {len(other.data)}"
)
result = [
[
sum(
self.data[i][k] * other.data[k][j]
for k in range(len(other.data))
)
for j in range(len(other.data[0]))
]
for i in range(len(self.data))
]
return Matrix(result)
def __rmatmul__(self, other):
# other @ self — only handles scalar multiplication
if isinstance(other, (int, float)):
return Matrix([[other * x for x in row] for row in self.data])
return NotImplemented
def __imatmul__(self, other):
# a @= b — mutates self in place
result = self.__matmul__(other)
self.data = result.data
return self
def __repr__(self):
return f"Matrix({self.data})"
Usage:
a = Matrix([[1, 2], [3, 4]])
b = Matrix([[5, 6], [7, 8]])
c = a @ b
# Matrix([[19, 22], [43, 50]])
a @= b
print(a)
# Matrix([[19, 22], [43, 50]])
NumPy Integration
NumPy implements __matmul__ on ndarray using optimized BLAS libraries under the hood. You do not need to implement __matmul__ yourself for NumPy arrays — it works out of the box.
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
c = a @ b
# array([[19, 22],
# [43, 50]])
NumPy’s @ operator is distinct from *, which performs element-wise multiplication:
result_elem = a * b
# array([[ 5, 12],
# [21, 32]])
Using operator.matmul
The operator module exposes the functional equivalent of @:
import operator
result = operator.matmul(a, b)
# equivalent to: a @ b
operator.matmul(a, b) calls a.__matmul__(b) internally and follows the same resolution chain. It is useful when you need to pass matrix multiplication as a function argument.
__matmul__ vs __mul__
Do not confuse @ with *. They have different semantics:
| Method | Operator | Semantics |
|---|---|---|
__mul__ | a * b | Element-wise multiplication |
__matmul__ | a @ b | Matrix/vector multiplication |
The difference matters especially in NumPy:
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
a * b # element-wise: [[5, 12], [21, 32]]
a @ b # matrix multiplication: [[19, 22], [43, 50]]
Common Mistakes
Returning self from __imatmul__ without mutating. The augmented assignment protocol requires __imatmul__ to return self, but you must actually mutate self before returning it. Forgetting to mutate and just returning self silently produces wrong results.
Using @ for element-wise operations. If you want element-wise multiplication, implement __mul__ and use *, not @ and __matmul__. Confusing the two is a source of subtle bugs, especially in numerical code.
Not returning NotImplemented when types are incompatible. Raise TypeError only for programming errors. For operator fallback to work correctly, return NotImplemented when the types are right but the operation is not implemented.
Conclusion
The @ operator and its dunder methods give you a way to define matrix multiplication semantics for custom objects. __matmul__ handles the standard case, __rmatmul__ provides a fallback when the left operand does not support @, and __imatmul__ handles @= augmented assignment — though the latter often results in a rebind rather than true in-place mutation depending on the object type.
For numerical computing in Python, NumPy’s built-in ndarray.__matmul__ handles everything efficiently using optimized libraries. For custom types, implement these methods to give your objects natural mathematical syntax.