3 Ways to Multiply Matrices in Python
Learn three reliable ways to multiply matrices in Python with NumPy, understand shape rules, batching, errors, and when each method fits.

For NumPy arrays, multiply matrices with A @ B or np.matmul(A, B). np.dot(A, B) also gives the conventional matrix product for two-dimensional arrays, while A * B performs elementwise multiplication. If A has shape (m, n), B must have shape (n, p), and the result has shape (m, p).
This guide explains the three approaches, shows complete runnable examples, covers vectors and batches, and diagnoses the errors that most often appear in real programs.
1. Use the @ operator
Python added @ and @= for matrix multiplication in Python 3.5 through PEP 465. The operator itself defines a protocol; NumPy arrays implement that protocol using matrix-multiplication semantics.
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]]
Each output entry is a row-by-column dot product. For example, the upper-left value is 1*5 + 2*7 = 19. The operator is usually the clearest choice in application code because it visually resembles the mathematics.
In-place multiplication with @=
A = A @ B
# or, when the existing array can be replaced:
A @= B
Assignment requires the left-hand array to be compatible with the product shape. Use ordinary assignment when changing shape is expected; use @= only when the resulting shape and dtype are appropriate for the existing variable.
2. Call np.matmul
np.matmul(A, B) expresses the same operation as A @ B. NumPy documents matmul as the implementation of the @ operator and recommends either form for two-dimensional matrix products. The function form is useful when explaining shapes, passing an operation as a callable, or using keyword arguments.

import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = np.matmul(A, B)
print(C)
The function also handles stacks of matrices. NumPy broadcasts the dimensions before the final two dimensions, then multiplies each corresponding pair of matrices.
A = np.array([
[[1, 0], [0, 1]],
[[2, 0], [0, 2]],
]) # shape (2, 2, 2)
B = np.array([
[[3, 4], [5, 6]],
[[7, 8], [9, 10]],
]) # shape (2, 2, 2)
C = np.matmul(A, B)
print(C.shape) # (2, 2, 2)
For a stack, the last two axes are the matrix axes. Earlier axes are batch axes and must be broadcast-compatible.
3. Call np.dot
np.dot(A, B) computes the conventional matrix product when both operands are two-dimensional. It remains common in older code and tutorials.
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = np.dot(A, B)
print(C)
For ordinary 2-D matrices, @, np.matmul, and np.dot produce the same values. For higher-dimensional arrays, their rules differ. NumPy’s dot documentation contracts the last axis of the first input with the second-to-last axis of the second input. matmul instead treats the final two axes as matrices and broadcasts the remaining axes. Choose @ or matmul when you mean batched matrix multiplication.
How matrix shapes determine whether multiplication works
For two-dimensional arrays, the inner dimensions must match:

| Operand | Shape |
|---|---|
A |
(m, n) |
B |
(n, p) |
A @ B |
(m, p) |
A = np.ones((3, 2))
B = np.ones((2, 4))
C = A @ B
print(C.shape) # (3, 4)
This fails because the inner dimensions differ:
A = np.ones((3, 2))
B = np.ones((3, 4))
C = A @ B # ValueError: ... size 3 is different from 2
Inspect shapes before multiplying:
print(A.shape, B.shape)
assert A.shape[-1] == B.shape[-2]
Do not “fix” a shape error by reshaping blindly. Confirm whether the data represents rows, columns, or batches, then use reshape, transpose (or .T for 2-D arrays), or an explicit axis move.
* is elementwise multiplication
NumPy’s array * operator multiplies corresponding elements, subject to broadcasting. It is not matrix multiplication. The two operations can look similar when arrays contain only ones, which makes this mistake easy to miss.
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A * B)
# [[ 5 12]
# [21 32]]
print(A @ B)
# [[19 22]
# [43 50]]
Use * for masks, per-feature scaling, and elementwise formulas. Use @ or matmul for linear algebra products.
Vectors, scalars, and one-dimensional inputs
matmul supports one-dimensional vectors, but the result shape can surprise you:
v = np.array([1, 2, 3])
M = np.eye(3)
print(v @ M) # vector, shape (3,)
print(M @ v) # vector, shape (3,)
print(v @ v) # scalar dot product
A one-dimensional vector is treated as a row or column temporarily, depending on its position, then the added dimension is removed. If you need an explicit column matrix, use v[:, None] or v.reshape(-1, 1). If you need an explicit row matrix, use v[None, :].
column = v[:, None] # shape (3, 1)
row = v[None, :] # shape (1, 3)
print(row @ column) # shape (1, 1)
Unlike matmul, np.dot has additional scalar behavior and different rules for arrays with three or more dimensions. Prefer @ when the code’s intent is matrix multiplication.
Choosing among the three methods
| Method | Best use | Important detail |
|---|---|---|
A @ B |
Readable application and scientific code | Uses NumPy’s matmul semantics |
np.matmul(A, B) |
Explicit calls and batched products | Broadcasts batch dimensions |
np.dot(A, B) |
Existing 2-D code and dot-product APIs | Higher-dimensional contraction differs |
A practical rule is: write @ for a normal matrix product, write np.matmul when its function form makes shape handling clearer, and keep np.dot when maintaining established two-dimensional code or when its specific contraction is intentional.
Reliable production patterns
Validate dimensions at boundaries
def multiply_matrices(A, B):
A = np.asarray(A)
B = np.asarray(B)
if A.ndim != 2 or B.ndim != 2:
raise ValueError("expected two-dimensional matrices")
if A.shape[1] != B.shape[0]:
raise ValueError(f"incompatible shapes: {A.shape} and {B.shape}")
return A @ B
Control numeric precision
Integer inputs produce integer results, so large products can overflow a fixed-width integer dtype. Convert deliberately when values may exceed that range:
A = np.asarray(A, dtype=np.float64)
B = np.asarray(B, dtype=np.float64)
C = A @ B
Floating-point output is approximate. Use a tolerance for comparisons:
np.testing.assert_allclose(actual, expected, rtol=1e-รักษ, atol=1e-12)
Replace the accidental non-ASCII value above with a normal tolerance such as rtol=1e-7; the point is to avoid exact equality for floating-point matrix products.
Keep batch axes explicit
Document shapes in variable names or comments, for example weights: (batch, output, input). This prevents a valid but unintended broadcast from silently producing the wrong result.
Performance, reliability, and cost notes
NumPy delegates large numerical operations to compiled implementations, but the actual speed depends on array sizes, dtype, memory layout, and the installed linear algebra backend. Avoid Python loops around individual scalar products; assemble arrays and perform one vectorized operation. Reuse arrays when profiling shows allocation overhead, and measure your own workload rather than assuming a universal benchmark.
For repeated products, check that operands have stable shapes and dtypes. Unexpected object arrays are much slower and can call Python code per element. Use np.asarray and inspect .dtype. For very large workloads, consider the memory cost of the output and intermediate arrays before increasing batch size.
Matrix multiplication itself has no network or service cost. If you are generating screenshots of a page that documents or displays these results, ScreenshotNeo can handle the browser work separately.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status.
Here is the one-call version; see the ScreenshotNeo documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://numpy.org/doc/stable/reference/generated/numpy.matmul.html -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://numpy.org/doc/stable/reference/generated/numpy.matmul.html"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://numpy.org/doc/stable/reference/generated/numpy.matmul.html' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Features include full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs, usage reporting, and an OpenAPI specification. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
There is no browser installation to maintain. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots, and every feature is available on every plan. Create a free ScreenshotNeo account.
Troubleshooting checklist
ValueError about a size mismatch
Cause: the inner dimensions do not match. Fix: print both shapes, transpose the operand only if the data model requires it, or reshape vectors explicitly.
The result is elementwise values
Cause: * was used. Fix: replace it with @ or np.matmul when you need a matrix product.
dot gives an unexpected shape
Cause: inputs have three or more dimensions and dot‘s contraction rules differ from matmul. Fix: use @/matmul for broadcasted stacks, or document the deliberate dot contraction.
Integer results overflow
Cause: fixed-width integer arithmetic wrapped around. Fix: convert to a sufficiently wide integer or floating dtype before multiplication.
Floating-point comparisons fail
Cause: rounding makes mathematically equal values differ by tiny amounts. Fix: use np.allclose or assert_allclose with tolerances.
FAQ
Which method should a beginner learn first?
Start with @. It is concise, readable, and matches NumPy’s matrix-multiplication semantics.
Is np.dot deprecated?
No. It remains available and is correct for two-dimensional matrix products. NumPy documentation generally favors @ or matmul when the intent is matrix multiplication.
Can I multiply Python lists with @?
Built-in lists do not implement matrix multiplication. Convert them with np.asarray or np.array first.
How do I multiply many matrices at once?
Put matrices in the final two axes of an array and use @ or np.matmul. Check that batch dimensions broadcast as intended.
How do I capture the explanation as a PDF?
Use ScreenshotNeo’s capture_pdf MCP tool or its PDF options through the API, with paper size, margins, orientation, and page ranges configured in the request.


