Index and Slice NumPy Arrays
Master indexing and slicing in NumPy with this hands-on tutorial. Learn to access and modify array elements using basic and advanced techniques, including boolean masks. Includes practical examples, troubleshooting tips, and next steps for your data science journey.
Focus: index and slice numpy arrays
You’ve got a CSV full of sales numbers, and you need the last quarter’s totals — but your Python loop crawls and your code reads like an archeology dig. Random access to data is the heart of analysis, and doing it wrong is the difference between a 30-second script and a 30-minute debugging session. In this lesson, you’ll master index and slice NumPy arrays — the tool that turns raw data into targeted insight, fast.
The problem this lesson solves
In pure Python, pulling out rows and columns from nested lists is verbose and slow. You write nested loops, carefully track indices, and pray you didn’t mix up [0][1] with [1][0]. For a data science workflow, that’s a productivity killer.
NumPy’s ndarray solves this with powerful, vectorized indexing and slicing. You get:
- Speed — operations run in C, not interpreted Python loops.
- Clarity — one line of
arr[1:5, 2:]replaces a mess of loops. - Flexibility — you can grab single elements, subarrays, every other row, or rows that meet a condition.
Pro tip: If you’ve been using Python lists for data, every loop you write is a candidate for a NumPy index. Convert to
np.array()and watch your code shrink.
The pain is real: without this skill, every analysis starts with manual, error-prone extraction. With it, you’ll spend your time on insight, not on plumbing.
Core concept / mental model
Think of a NumPy array as a grid of boxes, each holding a value. Indexing is opening a specific box; slicing is cutting out a rectangular region from the grid.
- Index —
arr[i]gets the element at positioni(0-based). For 2D arrays,arr[i, j]gets the element at rowi, columnj. - Slice —
arr[start:stop:step]gets a sequence of elements. For 2D,arr[row_start:row_stop, col_start:col_stop]gets a submatrix. - Boolean mask —
arr[arr > 5]gets all elements that pass a condition. - Fancy indexing —
arr[[0, 2, 4]]gets a specific set of rows.
The mental model: index picks single points, slice picks ranges, mask picks by condition, fancy picks by list. Once you see it this way, every extraction becomes a choice of one tool.
Key definitions
- Axis — each dimension of the array. A 1D array has one axis; a 2D array has two (rows, then columns).
- View vs. copy — slicing often returns a view (shares data), not a copy. Modifying a view modifies the original.
- Ellipsis (
...) — a shorthand for “all remaining axes”.
Pro tip: Keep the row, column order in mind.
arr[2]on a 2D array gives you row 3, not column 3. Mixing these up is the #1 beginner mistake.
How it works step by step
Step 1: Basic indexing
Start with a 1D array. arr[0] gives the first element, arr[-1] gives the last. For 2D arrays, arr[i, j] works like arr[i][j] but faster and cleaner.
Step 2: Slicing
The syntax [start:stop:step] is borrowed from Python lists but extends to multiple dimensions. For 2D, separate row and column slices with a comma: arr[1:3, 0:2].
Step 3: Boolean masking
Create a condition like arr > 5, get a boolean array, then use it directly as an index. This is the workhorse of data filtering.
Step 4: Fancy indexing
Pass a list of indices to grab non-contiguous elements. arr[[0, 2]] gets rows 0 and 2.
Step 5: Combine them
You can mix slicing with indexing, or boolean masks with slicing. For example, arr[arr[:, 0] > 5, 1:] — rows where the first column is >5, then all columns after the first.
Hands-on walkthrough
Example 1: Basic indexing and slicing
import numpy as np
# Create a 4x5 array of integers
data = np.arange(20).reshape(4, 5)
print("Original array:")
print(data)
# Get a single element (row 2, column 3)
print("data[2, 3] =", data[2, 3]) # Output: 13
# Slice the first two rows and columns 1-3
a_sub = data[0:2, 1:4]
print("Slice data[0:2, 1:4]:")
print(a_sub)
# Output:
# [[ 1 2 3]
# [ 6 7 8]]
# Slice every other row, all columns
even_rows = data[::2, :]
print("Even rows (step 2):")
print(even_rows)
Example 2: Boolean masking
import numpy as np
scores = np.array([65, 82, 91, 58, 74, 88])
# Find all passing scores (>60)
passing = scores[scores > 60]
print("Passing scores:", passing) # Output: [65 82 91 74 88]
# Replace all failing scores with 0
scores[scores < 60] = 0
print("After failing set to 0:", scores) # Output: [65 82 91 0 74 88]
# Use a condition on a 2D array: rows where row sum > 15
data2 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
row_sums = data2.sum(axis=1)
print("Rows with sum > 15:")
print(data2[row_sums > 15])
# Output: [[4 5 6]
# [7 8 9]]
Example 3: Fancy indexing and combining techniques
import numpy as np
# Create a 5x5 identity-like matrix
arr = np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25]])
# Fancy indexing: get rows 0, 2, and 4
selected_rows = arr[[0, 2, 4], :]
print("Rows 0, 2, 4:")
print(selected_rows)
# Output: rows 1, 3, 5
# Fancy indexing with columns: get columns 0 and 3
selected_cols = arr[:, [0, 3]]
print("Columns 0 and 3:")
print(selected_cols)
# Combine boolean mask on rows and slice on columns
# Rows where first column > 10, then columns 0-2
condition = arr[:, 0] > 10
result = arr[condition, :3]
print("Rows with first col >10, first 3 columns:")
print(result)
# Output: [[11 12 13]
# [16 17 18]
# [21 22 23]]
Example 4: Modifying with slices and views (advanced)
import numpy as np
base = np.arange(10) # [0 1 2 ... 9]
view = base[2:8] # slice returns a view
print("View initially:", view) # [2 3 4 5 6 7]
# Modify the view
view[0] = 99
print("Original after modifying view:", base)
# Output: [ 0 1 99 3 4 5 6 7 8 9]
# Note the original changed!
# To avoid that, use .copy()
base2 = np.arange(10)
copy_slice = base2[2:8].copy()
copy_slice[0] = -1
print("Original after modifying copy:", base2)
# Output: [0 1 2 3 4 5 6 7 8 9] # unchanged
Expected output — run these in a Jupyter notebook or a script. Each example prints clearly, so you can verify your understanding.
Compare options / when to choose what
| Technique | Best for | Example | Performance | Copy vs. view |
|---|---|---|---|---|
| Basic indexing | Single element | arr[2, 3] |
Fastest | Copy (scalar) |
| Slicing | Contiguous subarray | arr[1:3, 0:2] |
Fast | View (usually) |
| Boolean mask | Conditional selection | arr[arr > 5] |
Fast, vectorized | Copy (always) |
| Fancy indexing | Non-contiguous indices | arr[[0,2,4]] |
Moderate (copy) | Copy (always) |
Ellipsis (...) |
Simplify 3D+ slicing | arr[..., 0] |
Fast | View (usually) |
When to choose what:
- Use slicing when you know the positions of the range you need.
- Use boolean masks when your selection depends on values — filtering is the most common data science action.
- Use fancy indexing when you have a list of specific indices and they aren’t a simple step pattern.
- Use Ellipsis to avoid writing many
:in multi-dimensional arrays — your future self will thank you.
Pro tip: If you plan to modify the selected data and don’t want to affect the original, always call
.copy()on a slice. Views are efficient, but silent mutations are a debugging nightmare.
Variations in practice
- Using
np.ix_for fancy indexing on multiple axes:arr[np.ix_([0,2], [1,3])]gets a submatrix efficiently. - Transposing before slicing:
arr.T[1:3]works on columns as if they were rows — handy for column-wise operations. - Using
takefor fancy indexing with repeated indices:np.take(arr, [0,0,1], axis=1)lets you duplicate columns.
These are not just academic — they show up in real code every day.
Troubleshooting & edge cases
IndexError: too many indices
You tried arr[1, 2] on a 1D array. Fix: check arr.ndim and use the right number of indices.
arr = np.array([1, 2, 3])
# arr[1, 2] # IndexError
print(arr[1]) # Correct: 2
Slicing out of bounds doesn’t error
Unlike indexing (which raises IndexError), slicing silently clips. arr[1:100] on a 5-element array returns elements at 1..4. This can hide bugs.
arr = np.arange(5)
print(arr[1:100]) # [1 2 3 4] — no error!
Confusing rows and columns
arr[2] on a 2D array returns row 3. To get a column, use arr[:, 2].
Modifying a view mutates the original
Slicing returns a view in most cases. Use .copy() if you don’t want that.
Boolean mask shape mismatches
The mask must have the same shape as the array (or broadcastable). Check with mask.shape.
Specialty: arr[arr > 5] returns a 1D array
Boolean masks flatten the result. If you need to preserve structure, use np.where.
arr = np.array([[1, 6], [3, 8]])
mask = arr > 5
print(mask) # [[False True]
# [False True]]
print(arr[mask]) # [6 8] — flat!
Pro tip: When debugging, print both the array and the index you're using — that reveals 90% of indexing bugs.
What you learned & what's next
You now have the core superpower of data analysis: index and slice NumPy arrays with confidence. You can access single elements, slice ranges, filter with boolean masks, and select by fancy indices — all in fast, readable NumPy code. You understand the crucial difference between views and copies, and you can troubleshoot the common pitfalls.
This is the stepping stone to vectorized operations and aggregation — the next lesson in the track. There, you’ll combine your indexing skills with operations like sum, mean, and reshape to turn raw arrays into meaningful statistics. Master indexing now, and the next lessons will feel effortless.
Practice recap
Open a Jupyter notebook and create a 6×6 array of random integers. Use boolean masking to extract all values greater than 15, replace every odd value in the fourth column with 0, and then slice out the last three rows using fancy indexing. Print each result and confirm you get the expected shapes. This 10-minute exercise will cement every concept from this lesson.
Practice recap
Open a notebook and build a 6×6 array of random integers. Use boolean masking to pull out values greater than 15, set every odd number in the fourth column to 0, and extract the last three rows with fancy indexing. Print each result and verify the shapes — this quick drill locks in the core indexing patterns.
Common mistakes
- Mixing up row and column order:
arr[2]on a 2D array returns row 3, not column 3. Always usearr[row, col]. - Forgetting that boolean masks return a flattened 1D array, losing the original shape unless you use
np.where. - Modifying a slice (view) and unknowingly changing the original array — call
.copy()when you intend to work on a temporary copy. - Using a boolean mask with a different shape than the array, causing a broadcast error or unexpected result.
- Slicing out of bounds silently clips instead of raising an error, hiding logic bugs in your code.
Variations
- Use
np.ix_to perform fancy indexing on multiple axes efficiently, e.g.,arr[np.ix_([0,2], [1,3])]. - Transpose the array before slicing to work on columns as rows:
arr.T[1:3]. - Use
np.takefor repeated or advanced fancy indexing along a specific axis.
Real-world use cases
- Filtering out-of-stock products from an inventory array by stock count > 0 using a boolean mask.
- Extracting specific sensor readings (e.g., rows for a given hour, columns for temperature and humidity) from a 2D sensor log.
- Subsetting a time-series array to the last 30 days for a rolling average calculation.
Key takeaways
- Basic indexing
arr[i, j]grabs a single element; slicingarr[start:stop:step, ...]grabs a range. - Boolean masks filter by condition and always return a copy, often flattened.
- Fancy indexing with lists selects non-contiguous elements and also returns a copy.
- Slicing usually returns a view — modifying it modifies the original array; use
.copy()to prevent that. - Remember the axis order: rows, then columns. A single index on a 2D array refers to a row.
- Mastering indexing unlocks efficient data extraction for all subsequent NumPy and pandas work.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.