Skip to content

NumPy Matrix Multiplication: Complete Guide to np.dot, matmul, and @

Updated on

Matrix multiplication is fundamental to machine learning, computer graphics, signal processing, and scientific computing. But NumPy offers three ways to multiply matrices -- np.dot(), np.matmul(), and the @ operator -- and the differences between them are confusing. Using the wrong one can silently produce incorrect results, especially when working with arrays of different dimensions or when you confuse element-wise multiplication (*) with matrix multiplication.

This guide clarifies each method, shows when to use which, and provides practical examples for every common linear algebra pattern.

📚

Element-wise vs Matrix Multiplication

First, the critical distinction. The * operator performs element-wise multiplication, NOT matrix multiplication:

import numpy as np
 
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
 
# Element-wise multiplication (Hadamard product)
print(A * B)
# [[ 5 12]
#  [21 32]]
 
# Matrix multiplication
print(A @ B)
# [[19 22]
#  [43 50]]

Matrix multiplication follows the rule: C[i,j] = sum(A[i,k] * B[k,j]) for all k.

The @ Operator (Recommended)

The @ operator (introduced in Python 3.5) is the cleanest and most readable way to do matrix multiplication:

import numpy as np
 
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
 
C = A @ B
print(C)
# [[19 22]
#  [43 50]]

Shape Rules

For matrix multiplication A @ B to work, the number of columns in A must equal the number of rows in B:

import numpy as np
 
# (2, 3) @ (3, 4) = (2, 4)
A = np.ones((2, 3))
B = np.ones((3, 4))
C = A @ B
print(C.shape)  # (2, 4)
 
# (5, 3) @ (3, 2) = (5, 2)
A = np.random.randn(5, 3)
B = np.random.randn(3, 2)
C = A @ B
print(C.shape)  # (5, 2)
 
# Mismatch raises ValueError
# np.ones((2, 3)) @ np.ones((4, 2))  # ValueError

np.matmul()

np.matmul() does exactly what @ does. They are equivalent for all practical purposes:

import numpy as np
 
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
 
# These produce identical results
result1 = A @ B
result2 = np.matmul(A, B)
 
print(np.array_equal(result1, result2))  # True

Use @ for cleaner code. Use np.matmul() when you need to pass it as a function argument (e.g., to reduce).

np.dot()

np.dot() is older and behaves differently from @/matmul for higher-dimensional arrays:

import numpy as np
 
# For 1D arrays: dot product (scalar result)
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(np.dot(a, b))  # 32 (1*4 + 2*5 + 3*6)
 
# For 2D arrays: matrix multiplication (same as @)
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(np.dot(A, B))
# [[19 22]
#  [43 50]]

Key Difference: Higher Dimensions

For arrays with more than 2 dimensions, np.dot() and @ behave differently:

import numpy as np
 
A = np.random.randn(2, 3, 4)
B = np.random.randn(2, 4, 5)
 
# @ treats as batch of matrix multiplications: (2, 3, 4) @ (2, 4, 5) = (2, 3, 5)
result_matmul = A @ B
print(result_matmul.shape)  # (2, 3, 5)
 
# np.dot uses different broadcasting: sums over last axis of A and second-to-last of B
result_dot = np.dot(A, B)
print(result_dot.shape)  # (2, 3, 2, 5) -- different shape!

Recommendation: Use @ or np.matmul() for matrix multiplication. Use np.dot() only for explicit dot products of 1D vectors.

Method Comparison

Feature@ operatornp.matmul()np.dot()* operator
OperationMatrix multiplyMatrix multiplyDot productElement-wise
1D x 1DDot product (scalar)Dot product (scalar)Dot product (scalar)Element-wise
2D x 2DMatrix multiplyMatrix multiplyMatrix multiplyElement-wise
N-D batchedBatch matmulBatch matmulDifferent semanticsElement-wise
ReadabilityBestGoodOKN/A
Scalars allowedNoNoYesYes
RecommendedYesYes (functional)For 1D dot onlyFor Hadamard

Dot Product (Vectors)

The dot product of two vectors is the sum of their element-wise products:

import numpy as np
 
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
 
# All equivalent for 1D vectors
print(np.dot(a, b))    # 32
print(a @ b)            # 32
print(np.sum(a * b))    # 32

Applications

import numpy as np
 
# Cosine similarity
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
 
vec1 = np.array([1, 2, 3])
vec2 = np.array([4, 5, 6])
print(f"Cosine similarity: {cosine_similarity(vec1, vec2):.4f}")
# 0.9746
 
# Projection of a onto b
def project(a, b):
    return (np.dot(a, b) / np.dot(b, b)) * b
 
print(project(np.array([3, 4]), np.array([1, 0])))
# [3. 0.]

Matrix-Vector Multiplication

import numpy as np
 
# Linear transformation
A = np.array([[2, 0], [0, 3]])  # Scaling matrix
v = np.array([1, 1])
 
result = A @ v
print(result)  # [2 3]
 
# Applying weights in a neural network layer
weights = np.random.randn(10, 5)  # 10 outputs, 5 inputs
inputs = np.random.randn(5)
output = weights @ inputs
print(output.shape)  # (10,)

Batch Matrix Multiplication

For deep learning and batch processing, @ handles batches automatically:

import numpy as np
 
# Batch of 32 matrices, each 4x3, times batch of 32 matrices, each 3x5
batch_A = np.random.randn(32, 4, 3)
batch_B = np.random.randn(32, 3, 5)
 
result = batch_A @ batch_B
print(result.shape)  # (32, 4, 5)

Practical Examples

System of Linear Equations

import numpy as np
 
# Solve Ax = b
# 2x + 3y = 8
# 4x + y = 10
A = np.array([[2, 3], [4, 1]])
b = np.array([8, 10])
 
x = np.linalg.solve(A, b)
print(f"x = {x[0]:.2f}, y = {x[1]:.2f}")
# x = 2.20, y = 1.20
 
# Verify: A @ x should equal b
print(A @ x)  # [8. 10.]

Simple Neural Network Forward Pass

import numpy as np
 
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
 
# Network: 3 inputs -> 4 hidden -> 2 outputs
np.random.seed(42)
W1 = np.random.randn(4, 3) * 0.5  # First layer weights
b1 = np.zeros(4)
W2 = np.random.randn(2, 4) * 0.5  # Second layer weights
b2 = np.zeros(2)
 
# Forward pass
X = np.array([1.0, 0.5, -1.0])  # Input
h = sigmoid(W1 @ X + b1)         # Hidden layer
y = sigmoid(W2 @ h + b2)         # Output layer
 
print(f"Input:  {X}")
print(f"Hidden: {h}")
print(f"Output: {y}")

PCA Projection

import numpy as np
 
# Generate sample data
np.random.seed(42)
data = np.random.randn(100, 5)  # 100 samples, 5 features
 
# Center data
data_centered = data - data.mean(axis=0)
 
# Compute covariance matrix using matrix multiplication
cov = (data_centered.T @ data_centered) / (len(data) - 1)
 
# Eigendecomposition
eigenvalues, eigenvectors = np.linalg.eigh(cov)
 
# Project to top 2 components
top_2 = eigenvectors[:, -2:]  # Last 2 (largest eigenvalues)
projected = data_centered @ top_2
print(f"Original shape: {data.shape}")
print(f"Projected shape: {projected.shape}")  # (100, 2)

Visualizing Matrix Operations

For exploring the effects of matrix transformations on data, PyGWalker (opens in a new tab) lets you visualize projected data interactively in Jupyter:

import pandas as pd
import pygwalker as pyg
 
df = pd.DataFrame(projected, columns=['PC1', 'PC2'])
walker = pyg.walk(df)

FAQ

What is the difference between np.dot and np.matmul?

For 1D and 2D arrays, they produce the same result. The key difference is with higher-dimensional arrays: np.matmul() (and @) treats them as batches of matrices, while np.dot() uses a different broadcasting rule. For new code, prefer @ or np.matmul().

What does the @ operator do in NumPy?

The @ operator performs matrix multiplication, equivalent to np.matmul(). It was introduced in Python 3.5. For 2D arrays, A @ B computes the standard matrix product. For higher-dimensional arrays, it performs batched matrix multiplication.

Why does A * B give different results than A @ B?

A * B performs element-wise multiplication (Hadamard product), multiplying corresponding elements. A @ B performs matrix multiplication, following the rule C[i,j] = sum(A[i,k] * B[k,j]). These are fundamentally different operations.

How do I compute a dot product of two vectors in NumPy?

For 1D arrays (vectors), use np.dot(a, b), a @ b, or np.sum(a * b). All three return the same scalar result: the sum of element-wise products.

What shapes are required for matrix multiplication?

For A @ B, the last dimension of A must equal the second-to-last dimension of B. For 2D: (m, n) @ (n, p) = (m, p). The number of columns in A must equal the number of rows in B.

Conclusion

For matrix multiplication in NumPy, use the @ operator as your default -- it's the most readable and handles batched operations correctly. Use np.dot() only for explicit 1D vector dot products. Never confuse * (element-wise) with @ (matrix multiplication). Remember the shape rule: (m, n) @ (n, p) = (m, p), and you'll avoid the most common matrix multiplication errors.

📚