Array Views and Shallow Copies: Beware of Modification Propagation
After completing this topic, you will be able to:
- Understand the difference between views and copies in NumPy.
- Explain why slicing returns a view.
- Avoid unintended modification propagation.
Surprising Behavior
import numpy as np
original = np.array([1, 2, 3, 4, 5])sliced = original[1:4]
sliced[0] = 99
print(sliced) # [99 3 4]print(original) # [ 1 99 3 4 5] β The original array is also changed!Even though only sliced was modified, original also changed. This is not a bug, but rather a feature. NumPy slicing does not copy the data; instead, it returns a view that shares the same memory.
What is a View?
A view is a different way of looking at the same data.
Memory: [1] [2] [3] [4] [5]
β β β β β
original: [0] [1] [2] [3] [4]
sliced = original[1:4]
β β β
sliced: [0] [1] [2] β Points to the same memory!Because the data is not copied:
- Memory Efficient: Slicing a 1GB array requires no extra memory.
- Fast: Copying takes zero time.
- Modification Propagation: Modifying a view also modifies the original (be careful!).
When Views are Created
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
# 1. Slicing β Viewv1 = arr[2:5] # Viewv1.base is arr # True (it's a view of arr)
# 2. reshape β View (when possible)v2 = arr.reshape(3, 3) # Viewv2.base is arr # True
# 3. Transpose β Viewmat = np.array([[1, 2], [3, 4]])v3 = mat.T # Viewv3.base is mat # True
# 4. Changing dtype (with the same size) β Viewv4 = arr.view(np.int64) # ViewHow to check if it's a view
arr = np.array([1, 2, 3, 4, 5])sliced = arr[1:4]copied = arr[1:4].copy()
print(sliced.base is arr) # True β It's a viewprint(copied.base is None) # True β It's an independent copy (no base)If base is None, it means that the array owns its data (it's a copy); if base is not None, it's a view of another array.
When Copies are Created
arr = np.array([1, 2, 3, 4, 5])
# 1. Fancy indexing β Copyc1 = arr[[0, 2, 4]] # Copy!c1[0] = 99print(arr) # [1 2 3 4 5] β The original remains unchanged
# 2. Boolean indexing β Copyc2 = arr[arr > 3] # Copy!c2[0] = 99print(arr) # [1 2 3 4 5] β The original remains unchanged
# 3. Explicit copyc3 = arr.copy() # Copyc3[0] = 99print(arr) # [1 2 3 4 5] β The original remains unchanged| Operation | Result |
|---|---|
arr[2:5] (slicing) | View |
arr[[0,2,4]] (fancy indexing) | Copy |
arr[arr > 3] (boolean indexing) | Copy |
arr.reshape(...) | View (when possible) |
arr.copy() | Copy |
arr.flatten() | Copy |
arr.ravel() | View (when possible) |
Difference from pandas
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# Slicing in pandassubset = df[df["A"] > 1]subset["B"] = 99# SettingWithCopyWarning may occur!In pandas, view/copy behavior can vary depending on the situation, making it difficult to predict. This is why the SettingWithCopyWarning exists. A safe approach:
# Explicit copysubset = df[df["A"] > 1].copy()subset["B"] = 99 # No warning, the original remains unchanged
# If the goal is to modify the originaldf.loc[df["A"] > 1, "B"] = 99 # Modify the original directlyCommon Mistakes in Practice
Modifying the original array inside a function
def normalize(data): # Bad: If data is a view, the original will be modified data -= data.mean() return data
original = np.array([10.0, 20.0, 30.0])result = normalize(original[0:3]) # Slicing = View!print(original) # [β10. 0. 10.] β The original is changed!
# Good: Work on a copydef normalize_safe(data): result = data.copy() result -= result.mean() return resultUsing views to work with large datasets
# Analyze only a portion of a 10GB datasethuge_data = np.memmap("data.bin", dtype=np.float64, shape=(1_000_000_000,))
# View: No additional memorychunk = huge_data[1000:2000] # View β No memory copy
# Copy: Memory allocationchunk_copy = huge_data[1000:2000].copy() # Copy β 8KB allocatedWith large datasets, views can be used intentionally to save memory. Call copy() only when modification is necessary.
Comparison with Python Lists
Slicing in Python lists, in contrast to NumPy, always returns a copy.
# Python list: Slicing = always shallow copypy_list = [1, 2, 3, 4, 5]sliced = py_list[1:4]sliced[0] = 99print(py_list) # [1, 2, 3, 4, 5] β The original remains unchanged!
# NumPy: Slicing = View (shared)np_arr = np.array([1, 2, 3, 4, 5])sliced = np_arr[1:4]sliced[0] = 99print(np_arr) # [ 1 99 3 4 5] β The original is changed!Not understanding this difference can lead to serious bugs when transitioning to NumPy. Code that was safe in Python lists can destroy the original array in NumPy.
Shallow Copy vs. Deep Copy
import copy
nested = [[1, 2], [3, 4]]
# Shallow copy: Only the outer list is copied, the inner lists are sharedshallow = copy.copy(nested)shallow[0][0] = 99print(nested) # [[99, 2], [3, 4]] β The inner list is changed!
# Deep copy: All nested objects are copiednested = [[1, 2], [3, 4]]deep = copy.deepcopy(nested)deep[0][0] = 99print(nested) # [[1, 2], [3, 4]] β The original remains unchangedNumPy's .copy() copies the entire data, so it has the same effect as a deep copy. NumPy arrays do not contain other Python objects internally (only numerical data), so the distinction between shallow and deep copies is not meaningful, and .copy() is sufficient.
Decision Flowchart
Is an array operation required?
βββ Is it okay to modify the original?
β βββ Yes β Use a view (slicing as is)
β βββ No β Call .copy()
βββ Is there enough memory?
βββ Yes β Use .copy() for safety
βββ No β Use a view, be careful with modificationsKey Takeaways
| Concept | Summary |
|---|---|
| View | Shares the same memory. Modifying it also modifies the original. |
| Copy | Independent memory. Modifying it does not change the original. |
| Slicing | Returns a view (in NumPy). |
| Fancy/Boolean Indexing | Returns a copy. |
.base | None if it's a copy, otherwise it's a view. |
.copy() | Explicit deep copy. |
NumPy views are designed for performance optimization. They allow you to work with GBs of data without copying, but they come with the side effect of "modifying the original when you modify the view". The rule is simple: slicing = view, fancy/boolean = copy. When in doubt, use .copy().
Pandas 2.0 introduces Copy-on-Write (CoW) mode, where slicing results are automatically copied when modified. It can be enabled with pd.options.mode.copy_on_write = True and will eventually become the default behavior. However, NumPy still follows the explicit view/copy model, so it's important to master the rules in this topic.