pyguides

__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:

MethodOperatorWhen called
__matmul__a @ bPython calls a.__matmul__(b) first
__rmatmul__a @ bPython calls b.__rmatmul__(a) if a.__matmul__ returns NotImplemented
__imatmul__a @= bPython 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 operand
  • other — 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:

  1. Calls a.__matmul__(b)
  2. If that returns NotImplemented, calls b.__rmatmul__(a)
  3. If both return NotImplemented, raises TypeError

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:

MethodOperatorSemantics
__mul__a * bElement-wise multiplication
__matmul__a @ bMatrix/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.

See Also

  • mul — element-wise * multiplication
  • eq — equality comparison operator
  • get — attribute access descriptor protocol