Memoryview

memoryview lets you look at — and sometimes edit — the raw memory behind an object like bytearray without copying a single byte, which matters enormously when processing large binary data.

memoryview is a built-in type that exposes the internal buffer of an object supporting the buffer protocol (such as bytes, bytearray, and array-like objects) without copying its data. Slicing a bytes or bytearray object directly always creates a new copy; slicing a memoryview instead produces another view over the same underlying memory, which can be dramatically more efficient for large buffers.

The Problem: Copying Is Expensive

When you slice a bytes or bytearray object, Python allocates a brand-new object and copies the relevant bytes into it. For a small buffer this is negligible, but for large binary data — a multi-megabyte file buffer, a network payload, a chunk of sensor data — repeatedly slicing to inspect or pass around sub-regions can mean repeatedly copying large amounts of memory for no functional reason.

big_buffer = bytearray(10_000_000)   # ~10 MB
chunk = big_buffer[1000:2000]         # copies 1000 bytes into a new bytearray
# ... if you only needed to *read* those bytes, that copy was wasted work

The Buffer Protocol (Concept)

The buffer protocol is a C-level interface (CPython implementation detail, though the concept is part of the language's data model via __buffer__-style support) that lets an object expose its underlying memory directly, without copying, to code that knows how to consume it. bytes, bytearray, array.array, and NumPy arrays (a third-party library) are common examples of buffer-protocol objects. memoryview is the Python-level way to obtain and work with such a raw view.

data = bytearray(b"hello world")
view = memoryview(data)
type(view)     # <class 'memoryview'>
len(view)       # 11 -- same length as the underlying buffer

Creating and Reading a memoryview

memoryview(obj) wraps any buffer-protocol object. You can index it (getting an int, same as indexing bytes), slice it (getting another memoryview, not a copy), and convert it back to concrete types with .tobytes() or .tolist() when you actually need a standalone copy.

data = bytearray(b"hello world")
view = memoryview(data)

view[0]              # 104 (an int, like bytes indexing)
sub = view[0:5]       # a NEW memoryview, no data copied
sub.tobytes()          # b'hello' -- materialize an actual bytes copy when needed

Zero-Copy Slicing: The Core Benefit

Slicing a memoryview does not copy the underlying data — the new memoryview simply references a different region (offset and length) of the same buffer. This makes slicing an O(1) operation regardless of the buffer's size, versus O(k) for copying k bytes out of a bytes/bytearray slice.

big = bytearray(10_000_000)
view = memoryview(big)
chunk_view = view[1000:2000]     # instant -- no 1000-byte copy happens

# Only materialize a real copy when you actually need one standalone:
chunk_bytes = chunk_view.tobytes()   # NOW the copy happens, deliberately

Mutable Buffers: Editing Through a View

If the underlying object is mutable (a bytearray, not a bytes), writing through a memoryview slice modifies the original buffer directly — no copy-back step needed. This is useful for in-place binary editing, such as patching a header field in a large buffer without touching the rest of it.

data = bytearray(b"hello world")
view = memoryview(data)

view[0:5] = b"HELLO"    # writes directly into data's memory
print(data)                # bytearray(b'HELLO world')

# A memoryview over an immutable bytes object is read-only:
ro_view = memoryview(b"immutable")
ro_view.readonly           # True
# ro_view[0] = 65          # would raise TypeError

Shape, Format, and itemsize

A memoryview exposes metadata about the buffer it wraps: .format (a struct-style type code, 'B' for unsigned byte by default), .itemsize (bytes per element), .shape (dimensions, relevant for multi-dimensional buffers like NumPy arrays), and .nbytes (total size). For a plain bytearray, the view is one-dimensional with itemsize == 1.

view = memoryview(bytearray(b"data"))
view.format       # 'B' -- unsigned char
view.itemsize      # 1
view.shape          # (4,)
view.nbytes          # 4

Reinterpreting a Buffer with cast()

memoryview.cast(format) reinterprets the same underlying bytes as a different element type or shape, still without copying — for example, viewing a buffer of raw bytes as a sequence of 4-byte integers. This is a specialized tool typically used when processing structured binary data at speed.

import array

raw = bytearray(array.array("i", [1, 2, 3]).tobytes())  # 3 ints, 4 bytes each
view = memoryview(raw)
ints_view = view.cast("i")     # reinterpret as 4-byte signed ints
list(ints_view)                  # [1, 2, 3]

Practical Use Cases

memoryview matters most when working with large binary payloads: parsing network protocol frames without copying the whole packet for each field access, processing large files in fixed-size chunks in place, passing sub-regions of a buffer into socket.recv_into()/send()-style APIs that accept buffer-protocol objects directly, or interoperating with libraries like NumPy (third-party) that read/write buffers without going through Python-level copies. For small, everyday binary data, plain bytes/bytearray slicing is simpler and the copy cost is irrelevant — reach for memoryview when profiling or buffer size actually justifies it.

def process_in_chunks(buf: bytearray, chunk_size: int):
    view = memoryview(buf)
    for start in range(0, len(view), chunk_size):
        chunk = view[start:start + chunk_size]
        yield chunk   # caller reads/writes this slice with no copy

Releasing a memoryview

A memoryview holds a reference to (and, on some platforms, a lock on) the underlying buffer, which can prevent the original object from being resized or, in CPython, from being garbage collected while views remain open. Call .release() (or use it as a context manager with with memoryview(obj) as view:) when you're done with a view, especially before code that needs to resize or delete the underlying buffer.

data = bytearray(b"hello")
with memoryview(data) as view:
    print(view.tobytes())
# view is automatically released at the end of the 'with' block

Key Takeaways

memoryview provides a zero-copy view over any buffer-protocol object (bytes, bytearray, and others), avoiding the copy that ordinary slicing performs.

Slicing a memoryview returns another memoryview (still no copy); call .tobytes()/.tolist() to materialize an actual copy when one is needed.

A memoryview over a bytearray is writable — assigning to a slice edits the underlying buffer directly, in place.

A memoryview over an immutable bytes object is read-only.

Reach for memoryview when processing large binary buffers where copy overhead actually matters, not for small everyday byte manipulation.

Release a memoryview (.release() or a with block) when done, since it can pin the underlying buffer.

chunk = big_bytearray[1000:2000] # always copies, even if you only need to read view = memoryview(big_bytearray) chunk = view[1000:2000] # no copy; call.tobytes() only if a real copy is needed

Slicing bytes/bytearray directly always copies the sliced region. Slicing a memoryview instead references the same memory, which matters for large buffers where repeated copying is wasteful.

ro_view = memoryview(b"data") ro_view[0] = 65 # TypeError: cannot modify read-only memory mutable = bytearray(b"data") rw_view = memoryview(mutable) rw_view[0] = 65

A memoryview inherits the mutability of its underlying object -- wrapping an immutable bytes object produces a read-only view. Wrap a bytearray instead if you need to write through the view.

view = memoryview(data) #... later, code tries to resize data while view is still open -- fails view = memoryview(data) # use view... view.release() # now data can be resized

An open memoryview can prevent the underlying resizable buffer from being resized or deleted. Release the view (explicitly or via a 'with' block) once you're done with it.

What is the key advantage of memoryview over slicing bytes/bytearray directly?

Slicing a memoryview does not copy the underlying data -- it produces a new view referencing the same memory at a different offset/length, which is O(1) regardless of buffer size. Slicing bytes/bytearray directly always copies the sliced region, which costs time and memory proportional to its size.

What does memoryview[i] return, and how does that compare to memoryview[i:j]?

Indexing a single position returns an int (the byte value), same as indexing bytes. Slicing with [i:j] returns another memoryview object over the same underlying buffer, not a copy and not a list.

Can you write through a memoryview? Under what condition does this work?

Yes, if the underlying object is mutable (e.g. a bytearray) -- assigning to a memoryview slice writes directly into the original buffer. A memoryview over an immutable object like bytes is read-only, and attempting to assign to it raises TypeError.

What does .tobytes() do, and why would you call it on a memoryview?

It materializes the view's referenced data as a new, standalone bytes object (this does copy). You'd call it when you're finally ready to produce a concrete, independent copy of the data -- for example, to store, send elsewhere, or use after the original buffer might change.

What is the buffer protocol, and which built-in types support it?

The buffer protocol is a low-level mechanism (CPython implementation detail at the C level, though conceptually part of the data model) that lets an object expose its underlying memory for zero-copy access. bytes, bytearray, and array.array are common built-in examples; memoryview is the Python-level tool for working with any buffer-protocol object.

Why might you need to call .release() on a memoryview?

An open memoryview can hold a reference to (and sometimes lock) the underlying buffer, preventing the original resizable object from being resized or deleted while the view exists. Releasing it (directly or via a 'with' block) frees that hold once you're done reading/writing through it.

In what kind of scenario does memoryview provide a meaningful practical benefit over plain bytes/bytearray slicing?

When working with large binary buffers where repeated slicing/copying would be expensive -- parsing network protocol frames, processing large files in chunks, or passing sub-regions of a buffer into low-level I/O APIs (like socket.recv_into()) that accept buffer-protocol objects directly without an extra copy.

Write a function patch_header(buf, new_bytes) that overwrites the first len(new_bytes) bytes of a mutable bytearray buf in place, using a memoryview.

def patch_header(buf: bytearray, new_bytes: bytes) -> None: view = memoryview(buf) view[:len(new_bytes)] = new_bytes data = bytearray(b"XXXX-payload-data") patch_header(data, b"HDR1") print(data) # bytearray(b'HDR1-payload-data') memoryview(buf) gives a writable view over the bytearray (since bytearray is mutable); assigning new_bytes to a slice of that view writes directly into buf's memory without needing to rebuild or reallocate buf.

What does this print, and why does modifying data affect part?

It prints b'Zde'. Because part is a memoryview slice referencing the SAME underlying memory as data (not a copy), modifying data through direct indexing is immediately visible when part is later read -- this is the defining behavior of a zero-copy view.

Write a function chunks(buf, size) that is a generator yielding successive memoryview slices of buf, each of length size (the last chunk may be shorter), without copying any data.

from collections.abc import Iterator def chunks(buf: bytearray, size: int) -> Iterator[memoryview]: view = memoryview(buf) for start in range(0, len(view), size): yield view[start:start + size] data = bytearray(b"abcdefghij") for chunk in chunks(data, 3): print(chunk.tobytes()) # b'abc' b'def' b'ghi' b'j'

Wrap buf in a single memoryview, then yield successive slices of it using range(0, len(view), size) -- each yielded slice is a new memoryview object referencing a region of the same buffer, with no data copied until the caller explicitly converts one (e.g. with .tobytes()).

What does this code print?

It prints 'True' and then 'caught: TypeError'. A memoryview over an immutable bytes object reports readonly as True, and any attempt to assign to it raises TypeError, since the underlying buffer itself cannot be modified.

data = bytearray(b"abcdefgh")
view = memoryview(data)
part = view[2:5]
data[2] = ord("Z")
print(part.tobytes())
ro = memoryview(b"hello")
print(ro.readonly)
try:
    ro[0] = 72
except TypeError as e:
    print("caught:", type(e).__name__)