Indexing in multiple dimensions is the cornerstone of working with NumPy (and Dask/xarray) arrays — it lets you select individual elements, slices, views, or masked subsets across any number of axes using integer indices, slices, ellipses, newaxis, boolean masks, or advanced indexing. Correct multidimensional indexing ensures efficient access (views when possible), avoids unnecessary copies, enables vectorized operations, and prevents subtle bugs in analysis, visualization, or modeling. In 2026, mastering multi-axis indexing is essential — it powers everything from image cropping (height/width/channels), time series slicing (time × features), climate data extraction (time × lat × lon), and ML tensor manipulation, while integrating with pandas (labeled indexing), Polars (columnar selection), xarray (coordinate-based), and Dask (chunk-aware lazy views).
Here’s a complete, practical guide to indexing in multiple dimensions with NumPy: basic integer/slice indexing, boolean/advanced indexing, views vs copies, ellipsis/newaxis, real-world patterns (images, time series, grids), and modern best practices with type hints, performance, memory efficiency, and Polars/Dask/xarray equivalents.
Basic integer & slice indexing — select elements or sub-arrays along each dimension.
import numpy as np
# 3D array: depth × rows × columns
arr = np.arange(60).reshape(3, 4, 5)
print(arr.shape) # (3, 4, 5)
# Single element
print(arr[1, 2, 3]) # 38
# Slice along axes
print(arr[0, :, :]) # first depth slice (4×5)
print(arr[:, 1:3, 2:4]) # rows 1-2, columns 2-3, all depths
print(arr[::2, :, -1]) # every other depth, all rows, last column
# Trailing dimensions can be omitted (implies :)
print(arr[1, 2]) # same as arr[1, 2, :]
Boolean & advanced indexing — mask-based selection or integer arrays for arbitrary indexing.
# Boolean mask (same shape or broadcastable)
mask = arr % 2 == 0
print(arr[mask]) # 1D array of even numbers
# Advanced indexing with arrays
rows = np.array([0, 2])
cols = np.array([1, 3])
print(arr[1, rows, cols]) # arr[1, 0, 1] and arr[1, 2, 3]
# Mixed: slice + advanced
print(arr[:, rows, 2]) # all depths, selected rows, column 2
Views vs copies — slicing usually returns views (no copy, shared memory); advanced/boolean often returns copies.
view = arr[1:3, :, :] # view — modifying view changes arr
view[0, 0, 0] = 999
print(arr[1, 0, 0]) # 999 (changed!)
copy = arr[[0, 2], :, :] # advanced indexing ? copy
copy[0, 0, 0] = 777
print(arr[0, 0, 0]) # unchanged
Ellipsis & newaxis — powerful shortcuts for multi-D indexing.
# Ellipsis (...) = all omitted dimensions
print(arr[1, ..., 2]) # arr[1, :, :, 2] — all rows/columns, fixed depth & last dim
# Newaxis (None) — add singleton dimension
print(arr[np.newaxis, ...].shape) # (1, 3, 4, 5) — batch dimension
print(arr[..., np.newaxis].shape) # (3, 4, 5, 1) — channel dimension
Real-world pattern: indexing multi-dimensional time series or image stacks.
# Time series: time × features (100 days × 3 sensors)
ts = np.random.rand(100, 3)
# Last 20 days, sensor 1 & 2
recent_sensors = ts[-20:, [0, 1]]
print(recent_sensors.shape) # (20, 2)
# Image stack: time × height × width × channels
images = np.random.rand(50, 256, 256, 3) # 50 frames RGB
# Middle frame, center 100×100 patch, all channels
patch = images[25, 78:178, 78:178, :]
print(patch.shape) # (100, 100, 3)
# All frames, red channel only
red_channel = images[..., 0] # (50, 256, 256)
Best practices for multidimensional indexing. Prefer slicing for views — contiguous access faster, no copy. Modern tip: use Polars for columnar data — df.select(...) or .filter(...) — faster for tabular; NumPy for n-D numerical. Use ellipsis liberally — arr[..., -1] = last column in any shape. Use np.newaxis — add dimensions for broadcasting. Avoid advanced indexing when views needed — causes copies. Add type hints — def slice_3d(arr: np.ndarray[np.float64, (None, None, None)]) -> np.ndarray. Monitor memory — arr.nbytes vs slice.nbytes (views share memory). Use np.ascontiguousarray — force C-order before heavy ops. Use xarray — da.sel(...) or da.isel(...) — coordinate/label-based indexing. Use Dask arrays — da[slice] — lazy views. Test indexing — assert view.base is arr for views. Profile with timeit — compare strided vs contiguous access. Use arr.flags — check C/F_CONTIGUOUS, OWNDATA.
Indexing in multiple dimensions with NumPy selects elements/slices/views/masks across axes — integer/slice for basic, boolean/advanced for complex, ellipsis/newaxis for shortcuts. In 2026, prefer views for speed/memory, use Polars/xarray for labeled data, Dask for lazy large arrays, and always verify with flags and base. Master multi-D indexing, and you’ll access and manipulate arrays efficiently and correctly in any dimension.
Next time you need to slice a 3D+ array — index carefully. It’s Python’s cleanest way to say: “Give me exactly the piece of data I want — in any shape.”