Back to List

Array Views and Shallow Copies β€” Be Careful of Modification Propagation

Learn why slicing returns a view in NumPy, the conditions under which modification propagation occurs, and how to prevent it with copy().

Intermediate
|
10min
|
Verified (2026-07)
array viewshallow copydeep copymodification propagationNumPy memory
Progress0/17 (0%)

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

python
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.

text
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

python
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
# 1. Slicing β†’ View
v1 = arr[2:5] # View
v1.base is arr # True (it's a view of arr)
# 2. reshape β†’ View (when possible)
v2 = arr.reshape(3, 3) # View
v2.base is arr # True
# 3. Transpose β†’ View
mat = np.array([[1, 2], [3, 4]])
v3 = mat.T # View
v3.base is mat # True
# 4. Changing dtype (with the same size) β†’ View
v4 = arr.view(np.int64) # View

How to check if it's a view

python
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 view
print(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

python
arr = np.array([1, 2, 3, 4, 5])
# 1. Fancy indexing β†’ Copy
c1 = arr[[0, 2, 4]] # Copy!
c1[0] = 99
print(arr) # [1 2 3 4 5] β€” The original remains unchanged
# 2. Boolean indexing β†’ Copy
c2 = arr[arr > 3] # Copy!
c2[0] = 99
print(arr) # [1 2 3 4 5] β€” The original remains unchanged
# 3. Explicit copy
c3 = arr.copy() # Copy
c3[0] = 99
print(arr) # [1 2 3 4 5] β€” The original remains unchanged
OperationResult
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

python
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# Slicing in pandas
subset = 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:

python
# Explicit copy
subset = df[df["A"] > 1].copy()
subset["B"] = 99 # No warning, the original remains unchanged
# If the goal is to modify the original
df.loc[df["A"] > 1, "B"] = 99 # Modify the original directly

Common Mistakes in Practice

Modifying the original array inside a function

python
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 copy
def normalize_safe(data):
result = data.copy()
result -= result.mean()
return result

Using views to work with large datasets

python
# Analyze only a portion of a 10GB dataset
huge_data = np.memmap("data.bin", dtype=np.float64, shape=(1_000_000_000,))
# View: No additional memory
chunk = huge_data[1000:2000] # View β€” No memory copy
# Copy: Memory allocation
chunk_copy = huge_data[1000:2000].copy() # Copy β€” 8KB allocated

With 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
# Python list: Slicing = always shallow copy
py_list = [1, 2, 3, 4, 5]
sliced = py_list[1:4]
sliced[0] = 99
print(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] = 99
print(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

python
import copy
nested = [[1, 2], [3, 4]]
# Shallow copy: Only the outer list is copied, the inner lists are shared
shallow = copy.copy(nested)
shallow[0][0] = 99
print(nested) # [[99, 2], [3, 4]] β€” The inner list is changed!
# Deep copy: All nested objects are copied
nested = [[1, 2], [3, 4]]
deep = copy.deepcopy(nested)
deep[0][0] = 99
print(nested) # [[1, 2], [3, 4]] β€” The original remains unchanged

NumPy'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

text
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 modifications

Key Takeaways

ConceptSummary
ViewShares the same memory. Modifying it also modifies the original.
CopyIndependent memory. Modifying it does not change the original.
SlicingReturns a view (in NumPy).
Fancy/Boolean IndexingReturns a copy.
.baseNone 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.

πŸ’¬ Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...