Python memoryview: Slicing Without Copying Memory

Python's memoryview provides a zero-copy mechanism for accessing and slicing binary data by interfacing directly with the internal Buffer Protocol at the C level. Instead of allocating new memory and copying bytes whenever a slice is created—which is the default behavior for objects like bytes and bytearray—a memoryview creates an informational wrapper around existing memory. This article explains the underlying mechanics of memoryview, how the Python Buffer Protocol facilitates shared data access, and why slicing with it achieves an \(O(1)\) constant time complexity.

The Cost of Standard Slicing

When you slice built-in sequences like bytes, bytearray, or list, Python creates a brand-new object and copies the specified range of elements into newly allocated memory:

data = b"X" * 100_000_000  # 100 MB
chunk = data[:10_000_000]   # Copies 10 MB into a new bytes object

In this standard slice, the operation consumes an additional 10 MB of RAM and incurs an \(O(N)\) time cost proportional to the slice size. Repeating this inside a loop—such as consuming a stream of data from a network socket or file—quickly causes high memory churn and excessive garbage collection overhead.

The C-Level Buffer Protocol

To prevent unnecessary copying, Python implements the Buffer Protocol (PEP 3118) at the C-API level. This protocol allows Python objects to expose their internal raw memory arrays to other objects.

When a type implements the buffer protocol, it exports a C structure called Py_buffer. This structure contains critical metadata describing the memory:

How memoryview Slices Memory

A memoryview is a high-level Python object that wraps this underlying Py_buffer structure. It does not own the memory buffer itself; instead, it retains a reference to the original object that allocated the memory to prevent it from being deallocated while the view is active.

When you slice a memoryview, Python performs a shallow operation:

  1. Allocates a New Wrapper: Python instantiates a new, lightweight memoryview object.
  2. Adjusts the Pointer and Length: The new object copies the Py_buffer metadata from the source view rather than the data payload. It recalculates the buf pointer to start at the slice's offset and updates the len field to match the slice's size.
  3. Increments the Reference Count: The underlying memory owner's reference count is tracked, ensuring the original memory remains allocated.
large_data = bytearray(b"X" * 100_000_000)
mv = memoryview(large_data)

# Creates a new view pointing to the same memory address + offset
mv_slice = mv[10_000:20_000]

In the example above, mv_slice does not duplicate the 10,000 bytes. Its internal pointer simply starts at mv.buf + 10000, and its length is set to 10000.

Key Performance Advantages