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 objectIn 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:
buf: A pointer to the starting address of the memory block.len: The total length of the memory block in bytes.itemsize: The size in bytes of each element.readonly: A flag indicating whether the memory can be mutated.format: A string describing the data type (similar to thestructmodule syntax).shape,strides, andsuboffsets: Metadata defining dimensions for multi-dimensional arrays.
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:
- Allocates a New Wrapper: Python instantiates a new,
lightweight
memoryviewobject. - Adjusts the Pointer and Length: The new object
copies the
Py_buffermetadata from the source view rather than the data payload. It recalculates thebufpointer to start at the slice's offset and updates thelenfield to match the slice's size. - 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
- \(O(1)\) Time and Space
Complexity: Slicing a
memoryviewtakes constant time, regardless of whether the slice represents 10 bytes or 10 gigabytes. - In-Place Mutation: Slicing a mutable object like
bytearrayviamemoryviewallows in-place modifications to the original data:buf = bytearray(b"abcdef") view = memoryview(buf) sub_view = view[2:4] sub_view[:] = b"ZZ" print(buf) # Output: bytearray(b'abZZef') - Optimized I/O: Many standard library modules (such
as
socketandio) accept buffer-compatible objects. Methods likesocket.recv_into()orfile.write()can consume slices of amemoryviewdirectly, eliminating intermediate memory copies across pipeline stages.