Behavior is expected.
First of all, you need to understand how an operation such as x*y is actually performed. The python interpreter will first try to compute x.__mul__(y) . If this call returns NotImplemented it will try to compute y.__rmul__(x) . y.__rmul__(x) y is a suitable subclass of type x , in this case the interpreter will first consider y.__rmul__(x) and then x.__mul__(y) .
Now it happens that numpy processes the arguments differently, depending on whether it considers the argument scalar or massive.
When working with arrays * performs element-wise multiplication, while scalar multiplication multiplies the entire array record by a given scalar.
In your case, foo() is considered a numpy scalar, and thus numpy multiplies all elements of the array by foo . Moreover, since numpy does not know about the type foo it returns an array with dtype=object , so the returned object:
array([[0, 0], [0, 0], [0, 0]], dtype=object)
Note: the numpy array does not return NotImplemented when trying to calculate the product, so the interpreter calls the numpy array __mul__ , which, as we said, performs scalar multiplication. At this point, numpy will try to multiply each array entry by your โscalarโ foo() , and thatโs where __rmul__ your __rmul__ method, because the numbers in the array return NotImplemented when their __mul__ is called with the foo argument.
Obviously, if you change the order of the arguments to the initial multiplication, your __mul__ method will be called immediately and you will not have __mul__ any problems.
So, to answer your question, one way to handle this is to use foo inherit from ndarray , so the second rule applies:
class foo(np.ndarray): def __new__(cls):
However, a warning that subclassing ndarray not simple . Moreover, you may have other side effects since your class is now ndarray .