Which operations give you a second window onto the same numbers - the single most common source of NumPy surprises.
Overview
One buffer, several descriptions
Understanding views and copies is the single most important thing for writing correct NumPy code. A view shares memory with the original array. A copy has its own memory. The difference is invisible when you only read. It shows up the moment you write.
Every slice creates a view. That means modifying a slice modifies the original array. This is a performance feature. Copying a gigabyte array every time you take a slice would be wasteful. But it is also a correctness hazard. If you pass a slice to a function and that function writes to it, the original array changes. Nothing warns you.
An array is a block of contiguous memory plus a description. The description records the starting address, the shape, the dtype, and the strides. Strides say how many bytes to step for each axis. A view is a new description of the same block. Making a view changes the strides and the shape but not the data. That is why views are free to create. No data is copied.
Transpose is a view. Reshape is a view when the result is contiguous. Fancy indexing is a copy. Boolean indexing is a copy. np.array always creates a copy. np.asarray creates a view when the input is already an array with the right dtype.
The distinction matters for three reasons. First, correctness. Writing through a view changes the original. Writing through a copy does not. Second, memory. Views use no extra memory. Copies double it. Third, performance. Creating a view is instant. Creating a copy takes time proportional to the array size.
np.shares_memory tells you whether two arrays share the same underlying buffer. It is the definitive check. When you are unsure, call it. When debugging an unexpected mutation, call it on every array that might be a view of the one that changed.
This module covers how views are created, how copies are created, how to tell the difference, and how to force a copy when you need one. The examples show mutations propagating through views and stopping at copies.
An array is a block of memory plus a description. Where it starts, what shape it has. How many bytes to step for each axis. Those steps are the strides.
A view is a new description of the same block. Making one is free. Writing through it changes what every other description of that block sees.
A copy is a new block. It costs time and memory, and it's split.
Nearly each surprise in NumPy that isn't about broadcasting is about this gap.
Worth knowing
Slicing, reshaping, transposing and ravel give views. Boolean masks, fancy indexing, flatten, copy and astype give copies.
A view is a start, a shape and strides. Anything expressible that way is free; scattered positions are not, so they copy.
np.shares_memory(a, b) is the reliable check. .base shows the source, and .flags["OWNDATA"] whether it owns its buffer.
A function that writes into a slice of its argument modifies the caller's array. Copy at the boundary if you did not mean to.
A view keeps its base alive, so a small slice of a large array can hold all of it in memory.
ravel gives a view where it can and flatten always copies — the same distinction, packaged as two functions.
Views versus Copies
Which steps give you a second window onto the same numbers, and why it matters.
The rule, in one table
Regular strides give a view; scattered selections cannot, so they copy.
A view is a start, a shape and a set of strides. Anything describable that way is free; anything else needs new memory.
example_02.pyNumPy
import numpy as np
a = np.arange(12).reshape(3, 4)
print("a.strides :", a.strides, "bytes to step each axis")
print("a[1:3].strides:", a[1:3].strides, "<- same steps, different start")
print("a[:, 1].strides:", a[:, 1].strides, "<- step a whole row at a time")
print("a.T.strides :", a.T.strides, "<- the strides simply swapped")
print()
print("Scattered positions have no single step size, so a mask")
print("or an index list cannot be expressed this way - hence a copy.")
Output
How to tell what you are holding
Three ways to check, and the one to reach for by default.
example_03.pyNumPy
import numpy as np
a = np.arange(6)
v = a[1:4]
c = a[[1, 2, 3]]
print("np.shares_memory(a, v):", np.shares_memory(a, v))
print("np.shares_memory(a, c):", np.shares_memory(a, c))
print()
print("v.base is a :", v.base is a, " <- a view remembers its source")
print("c.base :", c.base, "<- a copy owns its data")
print()
print("v.flags.OWNDATA:", v.flags["OWNDATA"])
print("c.flags.OWNDATA:", c.flags["OWNDATA"])
print()
print("shares_memory is the honest check - base can be a chain of views.")
Output
The bug this causes
A function that slices its argument and writes to the slice modifies the caller's data.
example_04.pyNumPy
import numpy as np
def normalise_wrong(data):
"""Looks harmless. Writes through to the caller."""
body = data[1:]
body -= body.min() # in place, on a view
return data
original = np.array([100.0, 5.0, 9.0, 7.0])
keep = original.copy()
normalise_wrong(original)
print("caller's array before:", keep)
print("caller's array after :", original, "<- changed")
def normalise_right(data):
out = data.copy()
out[1:] -= out[1:].min()
return out
again = keep.copy()
result = normalise_right(again)
print()
print("with a copy, the input survives:", again)
print("and the result is :", result)
Output
A view can keep a big array alive
The base is referenced by the view, so slicing one row out of a huge array does not free the rest.
example_05.pyNumPy
import numpy as np
big = np.zeros((2000, 500))
print("big :", "%.1f MB" % (big.nbytes / 1e6))
row_view = big[0]
row_copy = big[0].copy()
print("view :", "%.4f MB of its own" % (row_view.nbytes / 1e6),
"but base is", "%.1f MB" % (row_view.base.nbytes / 1e6))
print("copy :", "%.4f MB, base:" % (row_copy.nbytes / 1e6), row_copy.base)
print()
print("Holding row_view keeps all 8 MB alive. If you are keeping a small")
print("slice of something large for a long time, copy it deliberately.")
Output
Forcing one or the other
copy() when you need independence; np.may_share_memory as the cheap guard in library code.
example_06.pyNumPy
import numpy as np
a = np.arange(6)
independent = a[1:4].copy()
independent[0] = 99
print("after writing to the copy, a is:", a)
print()
print("a cheap conservative check (may say True when unsure):")
print(" may_share_memory(a, a[1:4]) :", np.may_share_memory(a, a[1:4]))
print(" may_share_memory(a, a.copy()):", np.may_share_memory(a, a.copy()))
print()
print("np.copy and np.array(x, copy=True) do the same job:")
b = np.array(a, copy=True)
b[0] = -1
print(" a:", a, " b:", b)
Output
Where the line falls
Views: basic slicing, reshape (usually), .T and other transposes, ravel (usually), np.newaxis, and view().
Copies: boolean masking, fancy indexing, flatten, copy, astype, and most functions that return a new array.
The rule underneath is rote. If the selected elements can be given by a start and a regular stride per axis, a view works. A slice steps evenly. It can. A boolean mask picks scattered spots with no single step size, so it can't.
That is why the list isn't any and doesn't need storing once you've the reason.
Two entries carry an "often". reshape returns a view when the result can be strided over the existing layout. Copies when it can't. Often after a transpose. ravel is the same. Neither tells you which happened. This Is why the check below matters.
Checking
np.shares_memory(a, b) is the honest answer, and the one to use.
b.base points at the array a view derives from, or None for a copy. It's helpful but can be a chain. A view of a view has a base that is itself a view. So it isn't a solid equality test.
b.flags["OWNDATA"] says whether the array owns its buffer.
np.may_share_memory is a cheap conservative check: it can say True when it's merely unsure. That makes it right for a fast guard in library code and wrong for a definite answer.
The bug
The hands-on result is a function that modifies its caller's data without saying so.
def normalise(data):
body = data[1:]
body -= body.min() # in place, on a view of the caller's array
return data
data[1:] is a view. -= writes in place. The caller's array is now other. Nothing in the signature suggested it would be.
This is easy to write by accident. Each step looks innocent. Hard to spot in review. Two defences:
Copy at the boundary when a function shouldn't modify its input. data = data.copy() at the top is cheap insurance for small arrays and a deliberate choice for large ones.
Say so in the name when a function does modify in place. NumPy's own rule is that in-place variants are clear. np.sort returns a sorted copy, a.sort() sorts in place.
Memory: a view keeps its base alive
A view holds a link to the array it came from. The whole buffer stays in memory if any view of it exists.
Slice one row out of an 8 MB array and keep it: you're keeping 8 MB, not 4 KB.
That matters when you're extracting a small piece of a thing large to hold on to. A filtered subset, a header, one channel. copy() releases the rest. It's just the same shape of problem as holding a slice of a huge Python string. It shows up in long-running processes as memory that never comes back.
When to copy deliberately
Crossing an API boundary where the caller shouldn't see your changes, or you shouldn't see theirs.
Keeping a thing small from a thing large. So The large thing can be freed.
Before an in-place loop over data you didn't create.
When you want a promise rather than the "often a view" action of reshape and ravel.
all over else, views are the point. They're why slicing a gigabyte array costs nothing and why NumPy code can pass windows around freely. The goal isn't to avoid them but to know which one you're holding.
The two "usually" cases
Two steps in the view column carry a qualifier. Both are worth understanding rather than storing.
reshape returns a view when the new shape can be walked with a regular stride over the existing memory. On a freshly created contiguous array, that is always. After a transpose, it often isn't. The buffer is being read column-first. Flattening it row-first needs gathering scattered values.
ravel is the same. flatten is the version that always copies. Its existence is really just a way of asking for the promise.
If you need the opposite promise. A view or an error, never a silent copy. Assign to .shape right. a.shape = (3, 4) raises if a view is hopeless. This Turns an invisible speed problem into a visible failure.
copy is shallow, in the way that matters
a.copy() copies the buffer, and for a numeric array that is a full, split copy.
For an object array it isn't. The new array has its own array of pointers. Those pointers lead to the same Python objects. Modifying one of those objects is visible through both arrays.
copy.deepcopy(a) from the standard library copies the objects too.
This only arises with dtype=object. This Is uncommon and often worth avoiding. The failure is confusing when it happens because copy() did just what its name suggests at the level it operates on.
In-place operations on a fancy index
There's a subtle failure that is worth knowing before it costs you an afternoon.
a[[0, 0, 1]] += 1
You might expect element 0 to be incremented twice. It's incremented once.
this expands to a fetch, an add. A store: a[idx] = a[idx] + 1. The fetch makes a copy holding element 0 twice, both copies get 1 added. Then both are written back to the same place. The second overwriting the first.
np.add.at(a, [0, 0, 1], 1) is the unbuffered version that does what the syntax suggests, incrementing element 0 twice. each ufunc has an .at method for this.
This matters in any histogram-like build-up where indices repeat. The wrong version makes plausible undercounts rather than an error.
Tracking down a mutation bug
The symptom is always the same: an array changed and nothing in the visible code changed it.
The diagnosis is rote.
Find each place the array was derived from a thing else. A slice, a reshape, a transpose, a ravel. Each is a pick.
**Check with np.shares_memory.** Between the array that changed and each pick source. This is definitive.
Look for in-place ops on the shared side.+=, -=, *=, sort(), fill(). Set into a slice. Anything that modifies rather than rebinding.
Check function boundaries. A function that takes an array and slices it's the most common origin. The mutation is one level away from where you're looking.
The fix is nearly always a copy() at the boundary. The question of *which* boundary is answered by deciding who owns the data.
An ownership convention
Most of these problems disappear under a simple rule, applied always:
A function doesn't modify its args unless its name says so.
NumPy follows this itself. np.sort returns a sorted copy. a.sort() sorts in place. np.append returns a new array. The function forms are safe, the method forms aren't. The gap is solid enough to lean on.
For your own code, that means copying at the top of any function that will modify what it was given. Or, better, not modifying it at all and returning a new array.
The exception is a deliberate in-place API for large data. Here Copying would defeat the purpose. Those functions should say so in the name, take the output array by name, or both. out= is NumPy's own answer to this. It's a good pattern to copy. The caller supplies the destination. No one is surprised about what gets written.
When views are the point
None of this is an arg against views. They're the reason NumPy can pass windows of large arrays around for free. The reason slicing a gigabyte array costs nothing.
The goal isn't to copy defensively all over. That would discard the main gain. But to know which one you're holding at the moments it matters. Across a function boundary, before an in-place loop. When keeping a thing small from a thing large.
Usually a view: reshape, ravel. Both fall back to copying when the requested layout cannot be strided over the existing memory — typically after a transpose.
Always a copy: boolean masking, fancy indexing, flatten, copy, astype, np.array(x) by default, and nearly every function returning a computed result.
Neither: in-place operations, which return nothing and modify the buffer.
The underlying rule makes the list predictable: if the pick can be given by a start, a shape and a stride per axis, it's a view. If the spots are scattered, it can't be, so it copies.
Why astype always copies
Even a.astype(np.float64) on an array that is already float64 returns a copy by default. That surprises people who expect it to be a no-op.
astype(dtype, copy=False) returns the first when no conversion is needed. That is the form to use in a function that norms its input. Here Copying each call is waste.
The default of copying is defensive: astype is often called to give a thing the caller will modify. Returning a shared array would make that dangerous.
Checking, once more
np.shares_memory(a, b) — definitive, and the one to use.
np.may_share_memory(a, b) — conservative and cheap. Can say True when unsure. Right for a guard, wrong for an answer.
a.base — the array a view derives from, or None. Helpful. But A view of a view has a base that is itself a view. It isn't an equality test.
a.flags["OWNDATA"] — whether the array owns its buffer.
a.flags in full also reports contiguity. This Is what finds whether the next reshape or ravel will copy.
Questions people ask about Views versus Copies
How can I tell whether two arrays share memory? Use np.shares_memory(a, b), or check b.base is a. A view keeps a reference to the array that owns the data.
Which operations return views? Slices, reshape when possible, transpose, ravel when possible, and view. Basic indexing gives views.
Which operations return copies? Fancy indexing, boolean masking, flatten, astype and explicit .copy(). Arithmetic always produces a new array.
Why did changing b change a? b was a view of a, probably from a slice. Call .copy() when you create b if you want independent data.
Is a copy always safe? It is safe but costs memory and time. On big arrays, a stray copy in a loop is a common slow-down.
What is the pitfall with chained indexing and assignment? a[mask][0] = 1 assigns into a temporary copy and is lost. Combine the index into one expression such as a[mask] = 1.
The habits that prevent the bugs
Copy at the boundary of any function that will modify what it was given, unless the name says else.
Follow NumPy's own convention: functions return new arrays, methods modify in place. np.sort versus a.sort(). Making your own code follow the same rule means callers can predict it without reading the body.
Copy on purpose when keeping a thing small from a thing large. The base can be freed.
**Check with shares_memory** when a mutation appears from nowhere. It's the fastest route from symptom to cause. It gives a definite answer where thinking about which steps copy gives a probable one.
None of this argues against views. They're why slicing a gigabyte array is free and why array code can pass windows around without thought. The goal is to know which one you're holding at the three moments it matters. Across a function boundary, before modifying in place. When keeping a fragment of a thing large.
Check yourself
0 of 4
Answer without scrolling back up.
Which of these returns a copy rather than a view?
A boolean mask selects scattered positions with no single step size, so no stride description can express it. Slicing, transposing and reshaping all can.
What is the reliable way to check whether two arrays share memory?
`.base` can be a chain of views, and `may_share_memory` is conservative - it may say True when unsure. `shares_memory` gives the definite answer.
A function does `body = data[1:]` then `body -= body.min()`. What happens to the caller's array?
Each step looks innocent and the combination silently mutates the input. Copy at the boundary, or make the in-place behaviour explicit in the name.
Why can holding a one-row view of a large array waste memory?
Slicing one row from an 8 MB array and keeping it keeps all 8 MB. `.copy()` releases the rest - the same problem as holding a slice of a huge string.
Cheat sheet
Views versus Copies
Learn which NumPy operations return a view and which return a copy. Modify a slice and see the change appear, or not, in the original array.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.