Skip to content
Home Python NumPy Indexing and Slicing — Beyond the Basics

NumPy Indexing and Slicing — Beyond the Basics

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Python Libraries → Topic 26 of 51
NumPy indexing goes well beyond Python list slicing.
⚙️ Intermediate — basic Python knowledge assumed
In this tutorial, you'll learn
NumPy indexing goes well beyond Python list slicing.
  • Basic slicing returns a view — mutating it mutates the original. Call .copy() when you need independence.
  • Fancy indexing (integer arrays) always returns a copy, even when the indices are in order.
  • Boolean indexing filters by condition and returns a copy — the original is unchanged.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • NumPy offers four indexing methods: basic slicing (view), integer fancy indexing (copy), boolean indexing (copy), and field access for structured arrays.
  • Basic slices always return a view — they share memory with the original. Modifying the view changes the original.
  • Fancy indexing (integer arrays) always returns a copy, even if indices are in sequence.
  • Boolean indexing also returns a copy — the original array stays untouched.
  • Use np.shares_memory() or the .base attribute to check if an array is a view.
  • Biggest mistake: assuming a slice is independent; call .copy() explicitly when needed.
🚨 START HERE
Quick Debug: View vs. Copy Check
Three commands to diagnose whether an array is a view or copy.
🟡Need to know if arr is a view or copy
Immediate ActionCheck .base attribute
Commands
print(arr.base) # None = own data, otherwise = parent array
print(np.shares_memory(arr, original)) # True = view
Fix NowIf view and you need independence: arr = arr.copy()
🟡Want to ensure an operation returns a view
Immediate ActionUse only basic slicing (start:stop:step) with no integer lists or boolean masks
Commands
view = original[1:5, 2:7] # always view
print(np.shares_memory(view, original)) # True
Fix NowIf you must use integer lists, wrap with np.ix_ and slice the result
🟡Memory doubled after indexing a large array
Immediate ActionCheck if indexing used fancy or boolean
Commands
result = arr[[0, 2, 5]] # copy → memory spike
result = arr[0:6:2] # view → no extra memory
Fix NowReplace fancy indexing with slicing where possible; if not, accept the memory cost
Production IncidentCorrupted Training Data from an Unintended ViewA machine learning pipeline silently corrupted its training dataset because a NumPy slice was treated as an independent copy.
SymptomModel accuracy dropped from 92% to 37% after a week without any code changes. The training data was being modified during preprocessing.
AssumptionThe engineer assumed that slicing always creates a new array, so modifying the slice was safe.
Root causeA basic slice was used to select a subset of features, but subsequent data cleaning steps modified the slice, altering the original training array in place.
FixReplace the slice with .copy() or use fancy indexing for the feature subset (which returns a copy).
Key Lesson
Never assume a slice is independent — verify with np.shares_memory().When dealing with shared memory arrays, copy before mutation.If a pipeline includes multiple transformation steps, make explicit copies at the boundaries.
Production Debug GuideSymptom → Action guide for production incidents involving unexpected array mutations.
Two arrays change together when you only modify oneCheck if one is a view of the other. Use np.shares_memory(arr1, arr2) or arr1.base is arr2
Memory usage spikes after an indexing operationCheck if the indexing returned a copy (fancy/boolean) instead of a view (slice). Use np.shares_memory on the result.
Unexpected shape mismatch in downstream codeVerify the indexing method: basic slice reduces dimension only if using integers. Fancy indexing preserves dimensions with [].
Changes to a slice don't affect the original as expectedYou likely used fancy or boolean indexing somewhere, causing a copy. Revert to basic slicing if you need a view.

Basic Slicing — Views, Not Copies

Basic slicing uses integers, slices (start:stop:step), and np.newaxis. It always returns a view — a window into the same memory. This is the most common pattern and the source of most confusion.

Example · PYTHON
12345678910111213141516171819202122
import numpy as np

a = np.arange(12).reshape(3, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

# Slicing returns a view
view = a[1:, 2:]
print(view)
# [[ 6  7]
#  [10 11]]

# Modifying the view modifies the original
view[0, 0] = 99
print(a[1, 2])  # 99 — the original changed

# Use .copy() when you want independence
safe = a[1:, 2:].copy()
safe[0, 0] = 0
print(a[1, 2])  # still 99
▶ Output
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 6 7]
[10 11]]
99
99
Mental Model
Think of a view as a window
A slice is like a window into a warehouse — you can see and rearrange the items, but you're still looking at the original shelf.
  • The window doesn't own the items; it just points to them.
  • Any change you make through the window changes the shelf.
  • Calling .copy() builds a new shelf with identical items.
  • Always ask: "Do I need to modify the original? If not, .copy()."
📊 Production Insight
Production bug: A data pipeline sliced a large array for parallel processing and each subprocess modified its slice, corrupting the shared source array.
Fix: Extract slices with .copy() before distributing to workers.
Rule: If multiple consumers might modify a slice, copy it first.
🎯 Key Takeaway
Basic slicing = view.
Modify the slice, modify the original.
Use .copy() to break the link.

Integer Array Indexing — Fancy Indexing

Pass an array of indices to select specific elements. This always returns a copy, and the output shape matches the index array shape. It's called "fancy indexing" and gives you powerful reordering and selection.

Example · PYTHON
1234567891011121314151617181920
import numpy as np

a = np.array([10, 20, 30, 40, 50])

# Select elements at positions 0, 2, 4
print(a[[0, 2, 4]])  # [10 30 50]

# Reorder and repeat elements
print(a[[4, 4, 2, 0]])  # [50 50 30 10]

# 2D fancy indexing
m = np.arange(12).reshape(3, 4)
rows = np.array([0, 1, 2])
cols = np.array([0, 2, 3])
print(m[rows, cols])  # [0, 6, 11] — picks m[0,0], m[1,2], m[2,3]

# Use np.ix_ to select a submatrix (outer indexing)
print(m[np.ix_([0, 2], [1, 3])])
# [[ 1  3]
#  [ 9 11]]
▶ Output
[10 30 50]
[50 50 30 10]
[ 0 6 11]
[[ 1 3]
[ 9 11]]
📊 Production Insight
Fancy indexing copies data even if you select the same elements in order. This breaks lazy chaining: a[[0, 1, 2]] is a copy, not a view.
Memory spike: Fancy indexing a large array creates a new allocation; avoid in tight loops.
Rule: Prefer basic slicing for read-only access; use fancy indexing only when you need non-contiguous selection.
🎯 Key Takeaway
Integer array indexing = copy.
Output shape matches the index array, not the original.
Use np.ix_ for submatrix selection with broadcast semantics.

Boolean Indexing — Filtering Arrays

Pass a boolean array of the same shape to select elements where the condition is True. This is how you filter arrays without writing a loop. Always returns a copy.

Example · PYTHON
1234567891011121314151617
import numpy as np

temps = np.array([22.1, 18.4, 35.7, 29.3, 15.0, 38.2])

# All temperatures above 30 degrees
hot = temps[temps > 30]
print(hot)  # [35.7 29.3 ... wait, 29.3 < 30]
print(temps[temps > 30])  # [35.7 38.2]

# Compound conditions — must use & and |, not and/or
extreme = temps[(temps < 18) | (temps > 35)]
print(extreme)  # [18.4 ... ]
print(temps[(temps < 18) | (temps > 35)])  # [15.  38.2]

# np.where: replace values that fail the condition
clipped = np.where(temps > 35, 35.0, temps)
print(clipped)  # caps at 35.0
▶ Output
[35.7 38.2]
[15. 38.2]
[22.1 18.4 35. 29.3 15. 35. ]
📊 Production Insight
Boolean indexing is great for filtering but it creates a copy, so memory can double for large arrays.
Missing parentheses cause silent bugs: (temps < 18) | (temps > 35) without parentheses throws an ambiguous error.
Rule: Always wrap each condition in parentheses when using & or |.
🎯 Key Takeaway
Boolean indexing = filter + copy.
Parentheses matter: (cond1) & (cond2).
Use np.where for conditional replacement without a loop.

np.newaxis and Ellipsis

np.newaxis inserts a new dimension — it is just an alias for None. The ellipsis ... means 'all the dimensions in between'. These are crucial for broadcasting and nD array manipulation.

Example · PYTHON
123456789101112
import numpy as np

v = np.array([1, 2, 3])  # shape (3,)

# Turn a row vector into a column vector
col = v[:, np.newaxis]  # shape (3, 1)
print(col.shape)  # (3, 1)

# Ellipsis: useful for nD arrays
data = np.zeros((2, 3, 4, 5))
print(data[0, ..., 2].shape)   # (3, 4) — first slice, last slice, middle left alone
print(data[..., -1].shape)     # (2, 3, 4) — all dims except the last
▶ Output
(3, 1)
(3, 4)
(2, 3, 4)
📊 Production Insight
newaxis is a no-copy operation — it just changes the shape metadata. Use it to align dimensions for broadcasting without memory overhead.
Ellipsis can be ambiguous if you have many dimensions; use explicit colons for clarity.
Rule: Prefer np.newaxis over reshape when you only need to add a dimension for operations.
🎯 Key Takeaway
np.newaxis = None = insert dimension, no copy.
Ellipsis = 'all remaining dimensions'.
Use for broadcasting compatibility and clean nD indexing.

Combining Indexing Techniques — Power and Pitfalls

You can mix basic slicing with fancy indexing or boolean indexing in the same expression. The result follows the copy/view rules per axis: any axis using a slice stays a view, any axis using fancy/boolean becomes a copy. The combined result is always a copy if any axis uses fancy indexing.

Example · PYTHON
12345678910111213141516
import numpy as np

arr = np.arange(24).reshape(2, 3, 4)

# Basic slice on first axis, fancy on second: result is a copy
result = arr[0, [0, 2], :]  # shape (2, 4)
print(result.base is arr)  # False

# Basic slice on first, boolean on second: also copy
mask = np.array([True, False, True])
result2 = arr[0, mask, :]
print(result2.base is arr)  # False

# All axes basic: view
view = arr[0, :, 1:3]
print(view.base is arr)  # True
▶ Output
False
False
True
📊 Production Insight
Mixed indexing often surprises engineers: a mix that looks like slicing may still produce a copy if one axis uses fancy indexing.
Don't assume .base is None for copies; it may point to a larger intermediate.
Rule: When debugging, explicitly check np.shares_memory(arr, subset) to be sure.
🎯 Key Takeaway
Any fancy or boolean axis forces the entire result to be a copy.
Basic slices stay views only when all axes are slices.
Check memory sharing with np.shares_memory, not intuition.

🎯 Key Takeaways

  • Basic slicing returns a view — mutating it mutates the original. Call .copy() when you need independence.
  • Fancy indexing (integer arrays) always returns a copy, even when the indices are in order.
  • Boolean indexing filters by condition and returns a copy — the original is unchanged.
  • np.ix_ constructs an open mesh for selecting submatrices with fancy indexing.
  • np.newaxis is just None — it inserts a length-1 dimension without copying data.

⚠ Common Mistakes to Avoid

    Assuming a slice is a copy
    Symptom

    Modifying a slice changes the original array, causing silent data corruption in pipelines.

    Fix

    Use .copy() explicitly when you need independence. Verify with np.shares_memory().

    Using and/or instead of &/| for boolean conditions
    Symptom

    ValueError: The truth value of an array with more than one element is ambiguous.

    Fix

    Always use & (and) and | (or) with parentheses: (cond1) & (cond2). Never use Python's and/or.

    Confusing a[0] with a[[0]]
    Symptom

    IndexError or unexpected shape reduction when selecting a single row.

    Fix

    a[0] reduces dimensions (basic indexing); a[[0]] keeps dimensions (fancy indexing). Use a[0:1] for a slice that keeps dimensions.

    Not using np.ix_ for submatrix extraction
    Symptom

    Fancy indexing with two index arrays picks diagonal elements instead of block.

    Fix

    Use m[np.ix_(rows, cols)] for outer product selection; otherwise m[rows, cols] selects pairs.

    Modifying a sliced array in parallel processes
    Symptom

    Dataset gets corrupted because each process shares the same underlying memory.

    Fix

    Copy the slice before passing to workers: worker_data = data[chunk_slice].copy()

Interview Questions on This Topic

  • QWhat is the difference between a view and a copy in NumPy? How do you check which you have?Mid-levelReveal
    A view shares memory with the original array; modifications affect both. A copy has its own memory. Use arr.base is not None to check if it's a view, or np.shares_memory(arr, original) for a definitive answer. Basic slicing always returns a view; fancy and boolean indexing return copies.
  • QWhy does modifying a NumPy slice affect the original array?JuniorReveal
    Because slices return a view — a shallow reference to the same data buffer. This is by design to avoid unnecessary memory copies. Call .copy() explicitly when you need an independent array.
  • QHow does boolean indexing differ from fancy indexing in terms of memory?SeniorReveal
    Both return copies — neither returns a view. Boolean indexing uses a boolean mask to select elements; fancy indexing uses integer index arrays. Both allocate new memory. Boolean indexing is often more readable for filter operations, while fancy indexing allows arbitrary reordering and repetition.
  • QWhat does np.ix_ do and when would you use it?Mid-levelReveal
    np.ix_ converts two 1D arrays into index arrays that produce a submatrix (outer product) when used for indexing. Without it, m[rows, cols] picks a sequence of pairs. With np.ix_, m[np.ix_(rows, cols)] selects all combinations, returning a full rectangular block.
  • QExplain what happens when you mix basic slicing with fancy indexing in the same expression.SeniorReveal
    If any axis uses fancy indexing (integer arrays or boolean arrays), the entire result is a copy, even if other axes use basic slicing. For example, a[0, [1,2], :] returns a copy. If all axes use basic slicing, the result is a view.

Frequently Asked Questions

How do I know if an operation returns a view or a copy?

Check the base attribute. If arr.base is original returns True, arr is a view. You can also check np.shares_memory(arr, original). Basic slices are always views; fancy and boolean indexing are always copies.

Why can't I use 'and' and 'or' with NumPy boolean arrays?

Python's 'and'/'or' operate on the truthiness of objects, which for arrays raises an ambiguity error. NumPy uses & (bitwise AND) and | (bitwise OR) for element-wise logical operations. Always wrap each condition in parentheses: (a > 0) & (a < 10).

What is the difference between a[0] and a[[0]]?

a[0] is basic indexing and reduces the dimension by 1 — a[0] on a (3,4) array gives shape (4,). a[[0]] is fancy indexing and preserves dimensions — it gives shape (1,4). The difference matters when you need to keep the array 2D.

Can I get a view from fancy indexing?

No. Fancy indexing always returns a copy. If you need a view and have to use an array of indices, restructure your logic to use basic slicing (e.g., using [start:stop:step]) whenever possible.

What is the ellipsis (...) in NumPy indexing?

The ellipsis represents 'all the dimensions not explicitly listed'. For a 4D array, a[0, ..., 2] means a[0, :, :, 2]. It makes code cleaner for high-dimensional arrays.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousNumPy Broadcasting — How It Actually WorksNext →NumPy Shape Manipulation — reshape, flatten, ravel, transpose
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged