Create your own
Lesson illustration

NumPy: Element-Wise vs. Matrix Multiplication

Welcome back. In the previous lesson, you treated one-dimensional arrays as vectors: you added them, scaled them, and measured their Euclidean norms. You also saw a warning worth carrying forward: v * w multiplies entries position by position.

This lesson makes that warning precise for two-dimensional arrays. In NumPy, * means element-wise multiplication, while @ means matrix multiplication. They may both run when matrices are the same shape, but they answer fundamentally different questions. Plan for about 35–40 minutes, ending with a 15-line NumPy micro-challenge.


Two operations that look similar but do different work

Let

Element-wise multiplication: A * B

Element-wise multiplication pairs entries at the same row and column:

So:

In NumPy:

elementwise = A * B
# or: elementwise = np.multiply(A, B)

For ordinary element-wise multiplication, the inputs need the same shape, such as (2, 2) and (2, 2). NumPy can also permit some different shapes through broadcasting, which you will examine more closely later. The main idea is unchanged: every output position comes from multiplying corresponding input positions.

Element-wise multiplication is often useful when you deliberately want to reweight individual values: applying a mask, multiplying observations by sample weights, or scaling each feature column.

Matrix multiplication: A @ B

Matrix multiplication combines an entire row of the first matrix with an entire column of the second matrix. Each output entry is a sum of products:

For the upper-left output entry:

For the upper-right entry:

Completing the calculation gives:

In NumPy:

matrix_product = A @ B
# equivalent: matrix_product = np.matmul(A, B)

The @ operator is normally the clearest notation for matrix multiplication in NumPy code.


The shape rule for matrix multiplication

The mathematical shape rule tells you whether @ is valid:

The inner dimensions must match. The resulting matrix takes its number of rows from the first input and number of columns from the second.

For example:

Then:

This multiplication is valid because both inner dimensions are .

By contrast, A * B would fail for these particular shapes: (2, 3) and (3, 2) are not element-wise compatible. This is a useful contrast:

  • * asks: Can NumPy pair entries position by position?
  • @ asks: Does each row of the left matrix have the same length as each column of the right matrix?

When both inputs are square matrices of the same shape, as in the first example, both operations are allowed. That is also where mistakes are easiest to miss: the code executes, the output shape is identical, but the values mean something different.

To see the distinction worked through visually, watch this short segment from Multiplication of Matrix Using Numpy - Python Tutorial by DataMites.

Multiplication of Matrix Using Numpy - Python Tutorial

The video first shows how * pairs matching entries, then works through matrix multiplication as row-by-column dot products before demonstrating NumPy functions for the operation.

Watch element-wise multiplication to connect A * B with same-position products. Then watch row-column products, pausing after the first output entry and checking why it is a sum rather than one product. Finish with NumPy implementation; translate the demonstrated .dot() and np.matmul() calls into the more readable A @ B notation.


A second view: a matrix transforms rows into weighted combinations

The supplied diagram gives an equivalent way to understand the matrix product.

The diagram shows a left \(3 \times 3\) matrix multiplying a right \(3 \times 3\) matrix: each row of the output is formed by using the corresponding row of the left matrix as weights for a sum of the three color-coded rows of the right matrix.

Suppose the first row of a left matrix is:

and the rows of the right matrix are , , and . The first row of the product is:

The diagram colors , , and to make this visible. The coefficients , , and scale entire rows; then those scaled rows are added. This is fully equivalent to calculating each output entry with a row-column dot product.

This “weighted combination” perspective is particularly useful in machine learning. If is a feature matrix and is a vector of model weights, then:

combines every row of feature values into one prediction score per observation. In contrast:

uses broadcasting to multiply each feature column by its corresponding weight but leaves the result as a matrix. It has not yet summed features into scores.

For instance, if has shape (100, 5) and w has shape (5,):

  • X * w has shape (100, 5): each of five feature columns is reweighted.
  • X @ w has shape (100,): each row becomes one weighted sum, or one model score.

That distinction will become central when you build linear models and gradient-descent code.


Order matters for matrix multiplication

Element-wise multiplication is commutative when shapes are compatible:

because matching entries are simply multiplied in the opposite order.

Matrix multiplication generally is not commutative:

Using the earlier matrices:

but:

So do not swap the inputs merely because both shapes happen to allow multiplication. In applied work, the order encodes the meaning of the calculation: whether a transformation is applied before or after another transformation, for example.


Concept-Level Micro-Challenge: compare * with @

Type and run this 15-line program. It uses square matrices so that both operations are legal, then checks that they produce distinct results.

import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
elementwise = A * B
matrix_product = A @ B
print("A * B:\n", elementwise)
print("A @ B:\n", matrix_product)
print("element-wise shape:", elementwise.shape)
print("matrix-product shape:", matrix_product.shape)
assert A.shape == (2, 2)
assert B.shape == (2, 2)
assert elementwise.shape == (2, 2)
assert matrix_product.shape == (2, 2)
assert np.array_equal(elementwise, np.array([[5, 12], [21, 32]]))
assert np.array_equal(matrix_product, np.array([[19, 22], [43, 50]]))
assert not np.array_equal(elementwise, matrix_product)

Your important result is not merely that the assertions pass. Notice that both outputs have shape (2, 2), yet their values differ:

A * B:
 [[ 5 12]
  [21 32]]

A @ B:
 [[19 22]
  [43 50]]

The final assertion is the conceptual test: two matrices being the same shape does not make * and @ interchangeable.


A practical debugging routine

When a multiplication line produces an error or suspicious output, use this sequence:

  1. Inspect shapes first.

    print(A.shape, B.shape)
    
  2. State the intended result in words.
    Are you multiplying matching entries independently? Use *.
    Are you combining rows with columns into weighted sums? Use @.

  3. Predict the output shape before running the code.
    For A @ B, take the outer dimensions. For (2, 3) @ (3, 4), expect (2, 4).

  4. Manually calculate one output entry.
    For matrix multiplication, calculate the top-left value from the first row of A and first column of B. If it does not match, investigate shapes and operation choice.

  5. Avoid np.matrix.
    Work with ordinary np.array objects. NumPy arrays make the operation explicit: * for element-wise arithmetic and @ for matrix multiplication.


Takeaways

You can now distinguish the two multiplication operations that appear constantly in numerical Python:

  • A * B or np.multiply(A, B) multiplies matching entries and typically preserves the input shape.
  • A @ B or np.matmul(A, B) performs matrix multiplication: row-by-column weighted sums.
  • For matrix multiplication,
  • Matrix multiplication is order-sensitive; generally, A @ B differs from B @ A.
  • In machine learning, X @ w turns each feature row into a weighted prediction score, while X * w only reweights individual feature values.

Next, you will focus directly on vector dot products: the row-column calculation at the heart of every entry in a matrix product.

Can't find a good explanation? Sign up and we'll make it for you

Sign up