Which operations give you a second window onto the same numbers - the single most common source of NumPy surprises.
Overview
One buffer, several descriptions
An array is a block of memory plus a description: where it starts, what shape it has, and 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, and 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 is independent.
Nearly every surprise in NumPy that is not about broadcasting is about this distinction.
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 operations 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.
example_01.pyNumPy
Output
Why the line falls there
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
Output
How to tell what you are holding
Three ways to check, and the one to reach for by default.
example_03.pyNumPy
Output
The bug this causes
A function that slices its argument and writes to the slice modifies the caller's data.
example_04.pyNumPy
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
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
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 mechanical. If the selected elements can be described by a start and a regular stride per axis, a view works. A slice steps evenly, so it can. A boolean mask picks scattered positions with no single step size, so it cannot.
That is why the list is not arbitrary and does not need memorising once you have the reason.
Two entries carry an "usually". reshape returns a view when the result can be strided over the existing layout, and copies when it cannot — typically after a transpose. ravel is the same. Neither tells you which happened, which 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 is informative but can be a chain — a view of a view has a base that is itself a view — so it is not a reliable 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 is merely unsure. That makes it right for a fast guard in library code and wrong for a definite answer.
The bug
The practical consequence 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 different, and nothing in the signature suggested it would be.
This is easy to write by accident, because each step looks innocent, and hard to spot in review. Two defences:
Copy at the boundary when a function should not modify its input. data = data.copy() at the top is cheap insurance for small arrays and a deliberate decision for large ones.
Say so in the name when a function does modify in place. NumPy's own convention is that in-place variants are explicit — np.sort returns a sorted copy, a.sort() sorts in place.
Memory: a view keeps its base alive
A view holds a reference to the array it came from, so the whole buffer stays in memory as long as any view of it exists.
Slice one row out of an 8 MB array and keep it: you are keeping 8 MB, not 4 KB.
That matters when you are extracting a small piece of something large to hold on to — a filtered subset, a header, one channel. copy() releases the rest. It is exactly the same shape of problem as holding a slice of a huge Python string, and it shows up in long-running processes as memory that never comes back.
When to copy deliberately
Crossing an API boundary where the caller should not see your modifications, or you should not see theirs.
Keeping something small from something large, so the large thing can be freed.
Before an in-place loop over data you did not create.
When you want a guarantee rather than the "usually a view" behaviour of reshape and ravel.
Everywhere else, views are the point. They are why slicing a gigabyte array costs nothing and why NumPy code can pass windows around freely. The goal is not to avoid them but to know which one you are holding.
The two "usually" cases
Two operations in the view column carry a qualifier, and both are worth understanding rather than memorising.
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 usually is not — the buffer is being read column-first, and flattening it row-first requires gathering scattered values.
ravel is the same. flatten is the version that always copies, and its existence is really just a way of asking for the guarantee.
If you need the opposite guarantee — a view or an error, never a silent copy — assign to .shape directly. a.shape = (3, 4) raises if a view is impossible, which turns an invisible performance problem into a visible failure.
copy is shallow, in the way that matters
a.copy() duplicates the buffer, and for a numeric array that is a complete, independent copy.
For an object array it is not. The new array has its own array of pointers, but 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 duplicates the objects too.
This only arises with dtype=object, which is uncommon and generally worth avoiding, but the failure is confusing when it happens because copy() did exactly what its name suggests at the level it operates on.
In-place operations on a fancy index
There is 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 is incremented once.
The reason is that this expands to a fetch, an add, and a store: a[idx] = a[idx] + 1. The fetch produces a copy containing element 0 twice, both copies get 1 added, and 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. Every ufunc has an .at method for this.
This matters in any histogram-like accumulation where indices repeat, and the wrong version produces 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 mechanical.
Find every place the array was derived from something else. A slice, a reshape, a transpose, a ravel — each is a candidate.
Check with np.shares_memory. Between the array that changed and every candidate source. This is definitive.
Look for in-place operators on the shared side.+=, -=, *=, sort(), fill(), and assignment into a slice. Anything that modifies rather than rebinding.
Check function boundaries. A function that takes an array and slices it is the most common origin, because the mutation is one level away from where you are looking.
The fix is nearly always a copy() at the boundary, and 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 consistently:
A function does not modify its arguments 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 are not, and the distinction is reliable 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, where copying would defeat the purpose. Those functions should say so in the name, take the output array explicitly, or both. out= is NumPy's own answer to this, and it is a good pattern to copy: the caller supplies the destination, so nobody is surprised about what gets written.
When views are the point
None of this is an argument against views. They are the reason NumPy can pass windows of large arrays around for free, and the reason slicing a gigabyte array costs nothing.
The goal is not to copy defensively everywhere — that would discard the main benefit — but to know which one you are holding at the moments it matters: across a function boundary, before an in-place loop, and when keeping something small from something 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 essentially 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 selection can be described by a start, a shape and a stride per axis, it is a view. If the positions are scattered, it cannot 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 original when no conversion is needed. That is the form to use in a function that normalises its input, where copying every call is waste.
The default of copying is defensive: astype is usually called to produce something the caller will modify, and 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. Informative, but a view of a view has a base that is itself a view, so it is not an equality test.
a.flags["OWNDATA"] — whether the array owns its buffer.
a.flags in full also reports contiguity, which is what determines whether the next reshape or ravel will copy.
The habits that prevent the bugs
Copy at the boundary of any function that will modify what it was given, unless the name says otherwise.
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 deliberately when keeping something small from something large, so the base can be freed.
Check with shares_memory whenever a mutation appears from nowhere. It is the fastest route from symptom to cause, and it gives a definite answer where reasoning about which operations copy gives a probable one.
None of this argues against views. They are 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 are holding at the three moments it matters: across a function boundary, before modifying in place, and when keeping a fragment of something 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
An array is a block of memory plus a description: where it starts, what shape it has, and how many bytes to step for each axis. Those steps are the strides.
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.