Back to Blog
Python

Python memoryview Usage: Avoiding Copies in Binary Data

python memoryview usage: Learn how to use Python's memoryview to read and modify binary data without copying, including slicing, casting, writable views, and performan...

memoryviewbuffer protocolbinary datapython performancebytearray
A diagram showing a memoryview referencing the same underlying buffer as a bytearray, with a slice pointing into the buffer without copying.

When you slice a bytes or bytearray object in Python, the slice creates a new object and copies the data. For small payloads that copy is negligible, but for large binary buffers it adds allocation and CPU work. memoryview exposes the underlying buffer of an object directly, so you can read and modify data without copying. This article covers practical python memoryview usage: how to create views, what slicing actually does, when the view is writable, and where the performance benefit really appears.

Creating a memoryview and Inspecting Its Properties

memoryview() is a built-in constructor that accepts any object supporting the buffer protocol. Common examples are bytes, bytearray, array.array, and mmap objects.

data = bytearray(b"hello world") view = memoryview(data) print(view.nbytes) # 11 print(view.ndim) # 1 print(view.format) # 'B' print(view.itemsize) # 1

The view does not own the data. It references the same memory as data, so any change to the underlying object is visible through the view. The attributes nbytes, format, itemsize, shape, and strides describe the layout of the exposed buffer. For a one-dimensional bytearray, format is 'B' (unsigned byte) and itemsize is 1.

Indexing a memoryview returns an integer, not a one-byte bytes object:

print(view[0]) # 104

This is a common source of confusion for developers coming from bytes, where data[0] also returns an integer. The difference matters when you try to pass a single element to an API expecting bytes.

Slicing Without Copying Data

The main reason to use memoryview is that slicing returns another memoryview rather than a copy of the data.

data = bytearray(b"abcdef") view = memoryview(data) sub = view[2:5] print(sub.tobytes()) # b'cde' data[3] = ord("X") print(sub.tobytes()) # b'cXe'

The slice view[2:5] creates a new memoryview that points into the same buffer. When the original data is modified, the change is visible through sub. With a plain bytes slice, data[2:5] would return a new bytes object with its own copy, and later modifications to data would not affect it.

This behavior is useful when parsing large binary payloads. Instead of extracting several sub-buffers and copying each one, you can create views into the original buffer and pass them to parsing functions. The original buffer stays alive as long as any view references it.

Read-Only and Writable Views

Whether a memoryview is writable depends on the underlying object. A view over bytes is read-only; a view over bytearray is writable.

b = b"hello" view = memoryview(b) view[0] = 72 # TypeError: cannot modify read-only memory
ba = bytearray(b"hello") view = memoryview(ba) view[0] = 72 print(ba) # bytearray(b'Hello')

The readonly attribute reports this state:

print(memoryview(b).readonly) # True print(memoryview(ba).readonly) # False

When you need to modify binary data in place, use a bytearray or another writable buffer type as the source. Attempting to write through a read-only view raises TypeError, so checking readonly before a write loop can save you from a confusing failure deep inside parsing code.

Casting the Buffer to a Different Format

The .cast() method reinterprets the same underlying bytes using a different format. This is useful when you want to read multi-byte values such as unsigned shorts or floats without copying the data.

data = bytearray(b"\x01\x00\x02\x00") view = memoryview(data).cast("H") print(view[0]) # 1 on a little-endian machine print(view[1]) # 2

The format string follows the same codes used by the struct module: 'B' for unsigned byte, 'H' for unsigned short, 'I' for unsigned int, 'Q' for unsigned long long, 'f' for float, and 'd' for double. Casting changes the interpretation of the buffer, not its content, and it is subject to alignment and length constraints. The total number of bytes must be divisible by the new element size, and the cast must be compatible with the original format.

Casting is particularly useful when decoding binary file headers or network packets where fields are packed as multi-byte integers. It avoids the repeated unpacking calls that a struct.iter_unpack approach would require, though it does not handle endianness conversion for you. If the data uses a different byte order than the host machine, you still need to apply byteswap or read the values with an explicit endianness-aware method.

When memoryview Actually Improves Performance

The performance benefit of memoryview comes from avoiding copies, not from faster element access. Slicing a large bytes object copies the entire slice into a new allocation. Slicing a memoryview creates a small view object that references the existing buffer.

large = bytearray(10 * 1024 * 1024) view = memoryview(large) chunk = view[1000:2000] # no copy

The same slice on large as a bytes object would allocate a new 1000-byte object. For a single slice the difference is small, but in a loop that extracts hundreds of slices from a large buffer, the savings in allocation and copying add up.

The tradeoff is that memoryview objects are more constrained than bytes. Indexing returns integers, comparison with bytes requires .tobytes(), and the view keeps the underlying buffer alive until it is released. If you need a standalone bytes object to pass to a function that expects bytes, the .tobytes() call will copy, and the performance advantage disappears. Use memoryview when the consumer can work directly with the view, such as another function that accepts buffer-protocol objects, socket.send, or os.write.

Releasing the Buffer and Managing the View Lifecycle

A memoryview holds a reference to the underlying buffer. If you create many views over a large object and keep them around, the original buffer cannot be freed. Calling .release() drops the view's reference to the buffer.

view = memoryview(large_buffer) # use the view view.release()

After release, any operation on the view raises ValueError. If the view has child views created by slicing, releasing the parent raises BufferError because the child views still export the buffer. Release child views first.

parent = memoryview(data) child = parent[2:5] parent.release() # BufferError: memoryview has exported buffers child.release() parent.release() # now works

In CPython, memoryview objects are also freed by reference counting, so releasing is mostly relevant when you want to explicitly control when a large buffer can be reclaimed, or when you are holding views in a long-lived structure. Using a with block is not supported directly; the contextlib.closing helper or an explicit try/finally is the usual pattern when you need guaranteed release.

Common Mistakes When Working with memoryview

The most frequent errors come from treating memoryview like a bytes object.

First, indexing returns an integer, not a one-byte object. Code that does view[0] == b'a' fails; the correct comparison is view[0] == ord('a').

Second, slicing returns a view, not bytes. If you pass a slice to a function that expects bytes, you get a TypeError unless the function accepts buffer-protocol objects. Call .tobytes() explicitly when you need a real bytes object.

Third, a memoryview over bytes is read-only. Writing to it raises TypeError. If you need a writable view, start from a bytearray or another mutable buffer type.

Fourth, casting does not change endianness. The cast reinterprets raw bytes in the host byte order. For data with a different byte order, apply .byteswap() on the cast view or decode with struct explicitly.

Fifth, releasing a parent view while child views exist raises BufferError. Track child views and release them first, or avoid holding both parent and child views when the parent's lifetime is not important.

These constraints are not bugs; they are the direct consequence of memoryview being a zero-copy window into memory that someone else owns. Understanding who owns the buffer and when it can be released is the core of using memoryview correctly.

python memoryview usage: Practical Usage and Code Examples | RYUSLOG DEV