NumPy: ndarray Core Concepts
Last updated: 2026-08-26
NumPy's core data structure is ndarray — the N-dimensional array. It looks like a Python list, but underneath it's completely different: fixed type, contiguous memory, vectorized operations. Understanding ndarray's internals is the first step to using NumPy effectively.
1. What You'll Learn
- ❶ The essence of ndarray: homogeneous, fixed-type, contiguous memory
- ❷ Dimensions (ndim), shape, and size
- ❸ The intuition behind axes
- ❹ The dtype system and memory footprint
- ❺ ndarray vs list memory model comparison
2. A Data Analyst's True Story
(1) The Problem: Excel in Python, Confusion
Bob has a 3-row, 4-column Excel table. He stores it as a Python list of lists — 3 sublists nested inside an outer list. He wants to sum the second column, so he writes a for loop to iterate through each row. For 1 million rows, that takes 5 seconds. Worse, when a string sneaks into one of the rows, the loop doesn't fail until it hits that cell.
(2) The ndarray Solution
Charlie suggests Bob convert the table to an ndarray with np.array. NumPy flattens the data into a contiguous block of memory and performs the column sum in one C-level vectorized operation — the same 1 million rows take just 5 milliseconds. Types are enforced at creation time, so a stray string gets caught immediately, not halfway through the computation.
(3) The Payoff
- 1000x faster computation
- Type mismatches are caught at creation time
- Metadata like shape, axes, and strides make multi-dimensional operations intuitive
3. The Essence of ndarray
(1) Homogeneous Data Types
All elements of an ndarray must be the same type. This is fundamentally different from Python lists — lists can mix integers, strings, and even objects, while ndarray allows only one dtype.
import numpy as np
a = np.array([1, 2, 3])
print(a.dtype) # int64
b = np.array([1.0, 2.0, 3.0])
print(b.dtype) # float64
c = np.array([1, 2.0, 3])
print(c.dtype) # float64 (auto upcast)
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(2) Contiguous Memory Storage
Python lists store an array of pointers — each pointer points to a separate PyObject scattered across heap memory. ndarray, on the other hand, arranges all data in a single contiguous block of memory, with no pointer overhead and excellent CPU cache efficiency.
| Feature | Python list | ndarray |
|---|---|---|
| Element type | Any mix | Same dtype |
| Storage | Pointer array → scattered PyObjects | Contiguous memory block |
| Per-element overhead | 28+ bytes (PyObject header) | dtype-dependent (1/2/4/8 bytes) |
| Cache-friendly | Poor (pointer jumping) | Excellent (sequential access) |
| Type safety | None (caught at runtime) | Enforced at creation |
▶ Example
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: 0D/1D/2D/3D array properties (Difficulty ⭐)
import numpy as np
a0 = np.array(42) # 0D scalar
a1 = np.array([1, 2, 3]) # 1D
a2 = np.array([[1, 2], [3, 4]]) # 2D
a3 = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]]) # 3D
for name, arr in [("0D", a0), ("1D", a1), ("2D", a2), ("3D", a3)]:
print(f"{name}: ndim={arr.ndim}, shape={arr.shape}, size={arr.size}")
# 0D: ndim=0, shape=(), size=1
# 1D: ndim=1, shape=(3,), size=3
# 2D: ndim=2, shape=(2,2), size=4
# 3D: ndim=3, shape=(2,2,2), size=8
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
4. Dimensions, Shape, and Size
(1) ndim / shape / size
| Attribute | Meaning | Example (shape=(3,4)) |
|---|---|---|
ndim |
Number of dimensions (axes) | 2 |
shape |
Length of each axis, as a tuple | (3, 4) |
size |
Total number of elements = product of axis lengths | 12 |
import numpy as np
a = np.zeros((3, 4))
print(a.ndim) # 2
print(a.shape) # (3, 4)
print(a.size) # 12
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(2) Strides
strides tells you how many bytes to skip in memory to move one step along a given axis.
| Attribute | Meaning | shape=(3,4) dtype=int64 |
|---|---|---|
strides |
Stride per axis (bytes) | (32, 8) |
itemsize |
Bytes per element | 8 |
import numpy as np
a = np.zeros((3, 4), dtype=np.int64)
print(a.strides) # (32, 8)
# axis 0: move 1 row = 4 * 8 = 32 bytes
# axis 1: move 1 col = 1 * 8 = 8 bytes
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
▶ Example
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: Strides visualization (Difficulty ⭐⭐)
import numpy as np
a = np.arange(12, dtype=np.int32).reshape(3, 4)
print(a)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(f"shape: {a.shape}") # (3, 4)
print(f"strides: {a.strides}") # (16, 4)
print(f"itemsize: {a.itemsize}") # 4
# axis 0 stride: 4 elements * 4 bytes = 16
# axis 1 stride: 1 element * 4 bytes = 4
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
5. Intuition Behind Axes
Axes are the key to understanding ndarray operations. axis=0 is the outermost dimension, axis=1 is the next, and so on.
| Axis | Position in shape | Intuition | 2D shape=(3,4) | 3D shape=(2,3,4) |
|---|---|---|---|---|
| axis=0 | 0th | Outermost | Row direction (across rows) | Along the "layer" axis |
| axis=1 | 1st | Second | Column direction (across columns) | Along the "row" axis |
| axis=2 | 2nd | Innermost | — | Along the "column" axis |
Memory trick: axis=k corresponds to the direction of shape[k]. Doing sum/reduce along that axis "eliminates" that dimension.
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
print(a.shape) # (2, 3, 4)
print(a.sum(axis=0).shape) # (3, 4) — axis 0 eliminated
print(a.sum(axis=1).shape) # (2, 4) — axis 1 eliminated
print(a.sum(axis=2).shape) # (2, 3) — axis 2 eliminated
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
6. dtype and Memory Usage
(1) dtype Type System
| dtype | Description | Bytes | Range |
|---|---|---|---|
np.int8 |
Signed 8-bit integer | 1 | -128 ~ 127 |
np.int32 |
Signed 32-bit integer | 4 | -2^31 ~ 2^31-1 |
np.int64 |
Signed 64-bit integer | 8 | -2^63 ~ 2^63-1 |
np.float32 |
32-bit float | 4 | ~±3.4e38, 7-digit precision |
np.float64 |
64-bit float | 8 | ~±1.8e308, 15-digit precision |
np.bool_ |
Boolean | 1 | True/False |
np.complex128 |
128-bit complex | 16 | Two float64 |
(2) Memory Model: ndarray Is Not a Nested List
graph TB
A["ndarray object"] --> B["data pointer<br/>raw memory block"]
A --> C["dtype<br/>element type"]
A --> D["shape<br/>(3, 4)"]
A --> E["strides<br/>(32, 8)"]
B --> F["0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | ... | 2,3"]
style A fill:#4CAF50,color:#fff
style B fill:#2196F3,color:#fff
style F fill:#FF9800,color:#fff
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
(3) Shape-Memory Reference Table
| shape | ndim | size | dtype=float64 nbytes | strides |
|---|---|---|---|---|
| (10,) | 1 | 10 | 80 | (8,) |
| (3, 4) | 2 | 12 | 96 | (32, 8) |
| (2, 3, 4) | 3 | 24 | 192 | (96, 32, 8) |
| (5, 2, 3, 4) | 4 | 120 | 960 | (192, 96, 32, 8) |
▶ Example
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: dtype memory comparison (Difficulty ⭐)
import numpy as np
for dtype in [np.int8, np.int32, np.int64, np.float32, np.float64]:
a = np.zeros(1000, dtype=dtype)
print(f"{dtype.__name__:10s} itemsize={a.itemsize} nbytes={a.nbytes}")
# int8 itemsize=1 nbytes=1000
# int32 itemsize=4 nbytes=4000
# int64 itemsize=8 nbytes=8000
# float32 itemsize=4 nbytes=4000
# float64 itemsize=8 nbytes=8000
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
▶ Example
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: Verify reshape doesn't copy data (Difficulty ⭐⭐)
import numpy as np
a = np.arange(12, dtype=np.int64)
b = a.reshape(3, 4)
print(a.shape, b.shape) # (12,) (3, 4)
print(a.strides, b.strides) # (8,) (32, 8)
print(a.nbytes, b.nbytes) # 96 96
b[0, 0] = 999
print(a[0]) # 999 — same memory!
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
7. Comprehensive Example: 3D Array Deep Dive
▶ Example
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: Full property analysis of a 3×4×2 array (Difficulty ⭐⭐⭐)
import numpy as np
a = np.arange(24, dtype=np.int64).reshape(3, 4, 2)
print("=== Full array ===")
print(a)
# [[[ 0 1]
# [ 2 3]
# [ 4 5]
# [ 6 7]]
# [[ 8 9]
# [10 11]
# [12 13]
# [14 15]]
# [[16 17]
# [18 19]
# [20 21]
# [22 23]]]
print(f"ndim: {a.ndim}") # 3
print(f"shape: {a.shape}") # (3, 4, 2)
print(f"size: {a.size}") # 24
print(f"dtype: {a.dtype}") # int64
print(f"itemsize:{a.itemsize}") # 8
print(f"nbytes: {a.nbytes}") # 192 (24 * 8)
print(f"strides: {a.strides}") # (64, 16, 8)
# axis 0: 4*2*8 = 64 bytes per step
# axis 1: 2*8 = 16 bytes per step
# axis 2: 1*8 = 8 bytes per step
# Verify strides manually
expected_strides = (
a.shape[1] * a.shape[2] * a.itemsize, # 4*2*8 = 64
a.shape[2] * a.itemsize, # 2*8 = 16
a.itemsize # 8
)
print(f"manual strides: {expected_strides}") # (64, 16, 8)
print(f"match: {a.strides == expected_strides}") # True
# axis reduction
print(a.sum(axis=0).shape) # (4, 2)
print(a.sum(axis=1).shape) # (3, 2)
print(a.sum(axis=2).shape) # (3, 4)
> **Output:** Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
❓ FAQ
📖 Summary
- ndarray is a homogeneous, fixed-type, contiguous-memory N-dimensional array
ndimis the number of dimensions,shapeis the tuple of axis lengths,sizeis the total element count- Axes are numbered from outermost to innermost: axis=0 is outermost, axis=ndim-1 is innermost
stridesdescribes the memory offset for moving along each axis; reshape doesn't change the underlying datadtypedetermines element type anditemsize;nbytes = size * itemsize- ndarray's contiguous memory model vs the list's pointer-jumping model is the root of the performance gap
📝 Exercises
-
Beginner (Difficulty ⭐): Create four arrays of length 5000 with dtypes int8, int32, float32, and float64. Print each array's
nbytesand explain why different dtypes use different amounts of memory. -
Intermediate (Difficulty ⭐⭐): Given an ndarray with shape=(5, 3, 2) and dtype=int16, calculate its strides and nbytes by hand, then verify your calculation with code.
-
Advanced (Difficulty ⭐⭐⭐): Explain in your own words how an ndarray with shape=(2,3,4) is laid out in memory. Draw a diagram, label the strides, and show the memory offset for each element.