To multiply matrices in Python, use NumPy and the @ operator: A @ B. That is real matrix multiplication - rows times columns, the dot-product rule from linear algebra. Do not write nested loops for it, and do not use *: in NumPy, * is element-wise multiplication, a completely different operation. The two traps that catch everyone are using * when they meant @, and getting a shape mismatch because the inner dimensions do not agree. Get those two straight and matrix math in Python is one clean operator.
Table of contents
- The @ operator is the answer
- The * versus @ trap
- Shapes must line up
- Why not to write your own loops
- Vectors, lists, and 1-D arrays
- How this fits the rest of the stack
- FAQ
The @ operator is the answer
Since Python 3.5, @ is the matrix multiplication operator, and with NumPy it does exactly what you want:
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
A @ B
# [[19, 22],
# [43, 50]]
Each element of the result is the dot product of a row of A with a column of B - the standard matrix multiplication rule. A @ B reads cleanly and is the idiomatic modern form.
Two equivalent function calls do the same thing:
np.matmul(A, B) # identical to A @ B
np.dot(A, B) # same result for 2-D arrays
@ and np.matmul are the preferred forms for matrix multiplication. np.dot gives the same answer for 2-D arrays but has different behaviour for higher dimensions, so reach for @ by default and you will not be surprised.
The * versus @ trap
This is the mistake that produces wrong answers silently, because both operators run without error:
A * B # ELEMENT-WISE: multiply matching positions
# [[ 5, 12],
# [21, 32]]
A @ B # MATRIX: rows times columns
# [[19, 22],
# [43, 50]]
* multiplies element by element - position (0,0) times position (0,0), and so on. @ does true matrix multiplication. They give different results, and if you use * thinking it is matrix multiplication, your numbers are simply wrong with no error to warn you.
This is worse than a crash, because a crash tells you something is wrong. A silent wrong answer propagates through your whole computation. If a matrix calculation gives numbers you do not expect, the very first thing to check is whether you wrote * where you meant @. It is the single most common NumPy linear-algebra bug.
Shapes must line up
Matrix multiplication has a dimension rule: to compute A @ B, the number of columns in A must equal the number of rows in B. The result has A’s rows and B’s columns.
A = np.zeros((2, 3)) # 2x3
B = np.zeros((3, 4)) # 3x4
(A @ B).shape # (2, 4) - inner 3s match, outer 2 and 4 remain
C = np.zeros((2, 3))
A @ C # ValueError: shapes (2,3) and (2,3) not aligned
The inner dimensions - the 3 and the 3 - must match; the outer dimensions become the result’s shape. When they do not match, NumPy raises a shapes not aligned error, which is genuinely helpful: it tells you the operation is undefined, not that your code is broken.
When you hit that error, print the shapes with A.shape and B.shape and check the inner pair. Often the fix is a transpose - A @ B.T - to line the dimensions up. Reading the shape error rather than fighting it is the fastest way through.
Why not to write your own loops
You can multiply matrices with triple-nested loops in pure Python. Do not, except as a learning exercise:
# educational, but slow and unnecessary
def matmul(A, B):
result = [[0] * len(B[0]) for _ in range(len(A))]
for i in range(len(A)):
for j in range(len(B[0])):
for k in range(len(B)):
result[i][j] += A[i][k] * B[k][j]
return result
This is correct and it is dramatically slower than NumPy - often hundreds of times slower for realistic sizes. NumPy’s multiplication runs in optimized C, using vectorized CPU instructions and cache-friendly memory layouts that interpreted Python loops cannot touch.
The lesson generalizes: for numeric array work, if you are writing explicit element-by-element loops in Python, you are almost certainly leaving a large speedup on the table. Express it as a NumPy array operation and let the C layer do the work. A @ B is not just shorter than the loops - it is a different performance class entirely.
Vectors, lists, and 1-D arrays
A couple of practical wrinkles.
Plain Python lists do not support @ - you need NumPy arrays. Convert first:
A = np.array([[1, 2], [3, 4]]) # from a list of lists
For 1-D arrays, @ computes the dot product (a single number), and NumPy is smart about treating a 1-D array as a row or column vector as needed:
v = np.array([1, 2, 3])
w = np.array([4, 5, 6])
v @ w # 32 - the dot product, a scalar
M = np.array([[1, 2, 3], [4, 5, 6]])
M @ v # [14, 32] - matrix times vector
v @ w on two 1-D arrays gives their dot product. M @ v multiplies a matrix by a vector. NumPy handles the vector-as-row-or-column detail for you in these common cases, so you rarely need to reshape manually. When you do need an explicit column vector, v.reshape(-1, 1) turns a 1-D array into a proper column - useful when the automatic handling does not match what you intend.
How this fits the rest of the stack
Reaching for NumPy’s vectorized operations instead of Python loops is the same instinct that keeps compute-heavy code fast enough to run as a real service - let the optimized layer do the heavy lifting. When that numeric work is behind an API under load, the difference between @ and a triple loop is the difference between a fast endpoint and a timeout. The RunxBuild hosting calculator lays out the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team watches deploys, logs, and restarts as they happen.
Useful related references:
- Change Python Version: The Three Tools That Make It Stop Hurting
- Deploy a Python API for Free on RunxBuild
- django-admin Not Found: Fix the Command Without Guessing Which Python You Used
- Python services on RunxBuild
FAQ
How do I multiply matrices in Python?
Use NumPy and the @ operator: A @ B, where A and B are NumPy arrays. It performs real matrix multiplication using the rows-times-columns rule. np.matmul(A, B) is equivalent. Avoid writing nested loops, which are far slower.
What is the difference between * and @ in NumPy?
* is element-wise multiplication - it multiplies matching positions. @ is matrix multiplication - rows times columns. They give different results, and using * when you meant @ produces a silently wrong answer with no error, which is the most common NumPy linear-algebra bug.
Why do I get shapes not aligned in NumPy matrix multiplication?
Because the inner dimensions do not match. For A @ B, the number of columns in A must equal the number of rows in B. Print A.shape and B.shape to check the inner pair; often a transpose like A @ B.T lines them up.
Should I write my own matrix multiplication loop in Python?
No, except to learn. Nested-loop matrix multiplication in pure Python is correct but often hundreds of times slower than NumPy, which runs in optimized C with vectorized instructions. For any real numeric work, use A @ B and let NumPy do the computation.
Can I use the @ operator on Python lists?
No. @ requires NumPy arrays, not plain lists. Convert first with np.array(my_list_of_lists). For 1-D arrays, @ computes the dot product, and NumPy treats a 1-D array as a row or column vector as needed in common matrix-vector cases.