Views versus Copies

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.

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

A quick reference

Always a view: basic slicing, a[::k], a[::-1], .T, transpose, swapaxes, moveaxis, a[None], expand_dims, squeeze, view().

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.

  1. Which of these returns a copy rather than a view?

  2. What is the reliable way to check whether two arrays share memory?

  3. A function does `body = data[1:]` then `body -= body.min()`. What happens to the caller's array?

  4. Why can holding a one-row view of a large array waste memory?

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.

NUMPY · vizlearn.in/numpy/views_vs_copies.html

About the author

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.