2025, Oct 26 09:00

Stop Silent No-Ops: Understand NumPy Advanced Indexing vs Basic Slicing to Ensure In-Place Updates

Learn how chained NumPy indexing turns views into copies and causes no-op writes. Get rules and examples to use advanced indexing for in-place updates.

NumPy indexing can be deceptively simple until chaining starts to mix basic slicing with advanced indexing. A tiny change in the order of operations may switch writes from in-place updates to silently modifying a temporary copy. Below is a minimal case that captures the pitfall and how to avoid it.

Reproducible case

import numpy as np
idx = np.arange(2, 4)
arr_left = np.arange(5)
arr_left[:][idx] = 0  # modifies arr_left
print(arr_left)
arr_right = np.arange(5)
arr_right[idx][:] = 0  # does NOT modify arr_right
print(arr_right)

In the first assignment, the array is updated. In the second, nothing happens to the original data. The difference is not about assignment semantics; it’s about when you’re working with a view and when you’ve already moved to a copy.

What actually happens

The slice arr_left[:] is a view into the same memory as arr_left. That means any in-place modification routed through this view can still change the original data. In contrast, arr_right[idx] uses advanced indexing, which produces a brand-new, independent array. Once that copy is created, applying [:] to it only operates on the copy, not on arr_right.

This can be observed explicitly using the base attribute. If two arrays share the same underlying memory, the view’s base points to the original; a standalone copy has base set to None.

probe_view = arr_left[:]
print(probe_view.base is arr_left)  # True: shares memory
probe_copy = arr_right[idx]
print(probe_copy.base)               # None: independent copy

Even though arr_left and arr_left[:] are different Python objects with different identities, they share the same NumPy data buffer. That’s why writing through the view still mutates the original array.

Why the order matters

Assignment targets are evaluated left to right. In arr_left[:][idx] = 0, the left part arr_left[:] is a view, and then the assignment with [idx] is performed on that view, which writes back to arr_left’s memory. In arr_right[idx][:] = 0, the left part arr_right[idx] creates a copy up front; then [:] is applied to that copy, and the assignment only touches the copy.

The concise fix

There is no reason to chain [:][idx] or [idx][:] for this scenario. Just address the target directly with the indices you need. If the goal is to write into the original array using advanced indexing, keep the advanced index on the left-hand side of the assignment.

import numpy as np
idx = np.arange(2, 4)
arr = np.arange(5)
arr[idx] = 0  # directly modifies arr
print(arr)

When does NumPy create a view vs a copy?

NumPy’s own wording captures the rule succinctly:

Views are created when elements can be addressed with offsets and strides in the original array. Hence, basic indexing always creates views. [...] Advanced indexing, on the other hand, always creates copies.

buf = np.arange(5)
buf[:]        # view (basic indexing)
buf[1:3]      # view (basic indexing)
buf[1:3][:]   # view (basic indexing)
buf[[2, 3]]   # copy (advanced indexing)

Chaining matters because once you switch into advanced indexing and obtain a copy, any further slicing applies to the copy, not the original. Keeping the advanced index directly on the left-hand side of an assignment ensures your write targets the original array.

Notes on multidimensional indexing

With multiple dimensions, prefer a[x, y] when you are indexing different axes, rather than a[x][y]. The latter performs two separate indexing operations in sequence and can easily change semantics or create unnecessary copies. When x and y are arrays, a[x, y] extracts elementwise pairs [a[xv, yv] for (xv, yv) in zip(x, y)], not all combinations of x and y. To obtain all combinations, an approach like a[x][:, y] changes the selection behavior but produces a copy rather than a view.

Why this matters

In performance-sensitive or memory-aware code, silently working on copies leads to wasted time and unexpected results. Knowing which operations create views and which produce copies prevents accidental no-op assignments and helps keep memory traffic under control.

Takeaways

If you need to mutate data in place, put the advanced index directly on the left-hand side of the assignment, and avoid chaining indexing operations like [:][idx] or [idx][:]. Remember that basic slicing returns views, while advanced indexing returns copies. This mental model is enough to predict whether your write will land in the original array or drift into a temporary.

The article is based on a question from StackOverflow by Daniel Reese and an answer by mozway.