skip to content

What does memoryview give you that slicing a bytes object does not?

level: middleimportance: must knowfreq 46%

answer

  1. One of them moves no data
  2. A window onto someone else's memory
  3. Watch what len() counts after a cast
  4. tobytes copies, release ends the export

basics

~20 s

Slicing bytes copies the sliced region into a new object; memoryview wraps the original memory, so slicing a view costs nothing but a small window object. A view of a bytearray can also write straight through to the source.

solid answer

~40 s

`memoryview` exposes an object's memory through the buffer protocol without copying it. Slicing a view yields another view over the same bytes, so parsing a large frame costs no allocations, and assigning through a view of a `bytearray` mutates the original — views of `bytes` are read-only and raise `TypeError` on write. `cast()` reinterprets the region under a different item format, after which `len()` counts items rather than bytes and `nbytes` holds the byte count. `tobytes()` is the explicit copy back out. `release()`, or using the view as a context manager, ends the export — which matters because a `bytearray` cannot be resized while any view of it is alive; it raises `BufferError`.

code

python · 12 lines
python
frame = bytearray(b"\x00\x01" * 8)
view = memoryview(frame)
header = view[:4]           # a view, not a copy
header[0] = 0xFF            # writes straight through to frame
print(frame[:4])
words = view.cast("H")      # reinterpret as 2-byte unsigned items
print(len(view), len(words), words[0])
print(header.tobytes())     # an explicit copy
header.release()
words.release()
view.release()
frame += b"\x00"            # only legal once no view is exported

go deeper

for a junior

Recall the headline: slicing bytes copies, wrapping in memoryview does not. Knowing that a view points at someone else's memory, and that tobytes() is how you get a real copy back, is enough at this level.

for a middle

Explain the mechanics you would use: write-through on a bytearray versus read-only views of bytes, cast changing what len() counts, and release ending the export. Be ready to say why the resize raises BufferError.

for a senior

Demonstrate judgement about when the copy actually hurts — large frames, hot parse loops, handing regions to native code — and about release discipline, since a forgotten view both pins memory and locks the buffer against growth.

for a principal

Own the tradeoff as an interface decision: exposing views across a module boundary exports lifetime and mutability obligations to callers. Decide where buffers are frozen with tobytes(), and treat toreadonly() as the contract when sharing writable memory.

### Slicing copies; viewing does not `data[10:20]` on a `bytes` or `bytearray` object allocates a new object and memcpys ten bytes into it. That is cheap at ten bytes and ruinous at ten megabytes, and it is what `memoryview` exists to avoid. `memoryview(data)[10:20]` allocates only a small view object that records a pointer, a length, an item format and a reference to the exporting object. No payload bytes move. A `memoryview` is a *window*, not a container. It has no storage of its own. Everything it can do is defined by the **buffer protocol**, the C-level interface an object implements to say "here is my memory, here is its shape, and here is whether you may write to it". `bytes`, `bytearray`, `array.array`, `mmap.mmap` and many C extension types all export it; since Python 3.12 the concept also has a name in the standard library, `collections.abc.Buffer`. ### Writing through a view Because a view points at the original memory, assigning through a view of a **mutable** exporter edits the original: given `view = memoryview(frame)` over a `bytearray`, `view[0] = 0xFF` changes `frame`. Views of immutable exporters are read-only — `memoryview(b"x")[0] = 1` raises `TypeError: cannot modify read-only memory` — and `readonly` reports which kind you hold. `toreadonly()` hands out a read-only view of writable memory, which is how you share a buffer without granting write access. This is the whole point in parsing code: you can walk a large received frame, hand each field to a handler as a view, and let a handler that must mutate do so in place, with zero intermediate allocations. ### cast, and why len() stops meaning bytes `cast(fmt)` reinterprets the same bytes under a different item format: `view.cast("H")` treats the region as unsigned 2-byte integers, `cast("i")` as 4-byte signed ones, `cast("B")` returns to plain octets. Nothing is copied or converted — only the interpretation changes. Consequently `len(view)` counts **items, not bytes**: a 16-byte region is `len() == 16` as `"B"` and `len() == 8` after `cast("H")`. The byte count lives in `nbytes`, the per-item width in `itemsize`, and the code in `format`. `cast` requires a C-contiguous view whose current format is plain bytes, and multi-byte formats use the machine's native byte order — which is why a view cast is the wrong tool for decoding a fixed-endian wire format. ### tobytes and release `tobytes()` is the deliberate exit hatch: it copies the viewed region into a new immutable `bytes` object. Call it exactly when you *want* the copy — at an API boundary, or when you intend to keep a small field long after the big source buffer should have been freed. `release()` tears the view down immediately rather than waiting for garbage collection, and a `memoryview` also works as a context manager, so `with memoryview(buf) as view:` releases on exit. Releasing matters because a live export **locks the exporter against resizing**: while any view of a `bytearray` is outstanding, `buf += b"x"`, `buf.extend(...)` or a length-changing slice assignment raises `BufferError: Existing exports of data: object cannot be re-sized`. CPython refuses because a reallocation would move the memory and leave the view dangling. After the view is released, the same resize succeeds. Any operation on a released view raises `ValueError`. ### Reading the view itself Four attributes describe any view and are worth naming in an answer: `readonly` says whether writes are permitted, `format` gives the item code, `itemsize` the width of one item in bytes, and `nbytes` the total size of the region regardless of format. Together they let you assert what you are holding before you touch it, which matters because a view arriving from elsewhere may be read-only, may already be cast, or may be over an exporter you did not create. Views can also describe more than a flat run of bytes — `shape`, `strides` and `ndim` exist for multi-dimensional and non-contiguous buffers exported by C extensions — but `cast` and the simple slicing described here require a contiguous one-dimensional region. After `release()`, every operation on the view raises `ValueError`, so a released view fails loudly rather than reading freed memory. ### What it is not A `memoryview` is not an I/O buffer, and it is not a general-purpose array. It does not convert types, does not resize, does not own memory, and does not make a slow loop fast on its own — a per-item Python loop over a view is roughly as slow as any other Python loop. Its payoff is allocation and copy avoidance on large regions: parsing a frame in place, filling a preallocated destination with a `readinto`-style call and then handing out subviews, or passing a region into a C extension that can operate on foreign memory. Below a few kilobytes, plain slicing is usually simpler and fast enough, and the view's bookkeeping — release discipline, resize locking, item-vs-byte lengths — is real complexity you should only take on when the copy actually hurts.

  • After view.cast('H'), why does len(view) change without any data changing?
    `len()` on a `memoryview` counts items, not bytes, and `cast` changes the item format. A 16-byte region has length 16 under the default `"B"` format and length 8 after casting to `"H"`, the 2-byte unsigned format. The bytes are untouched; only the interpretation moved. `nbytes` still reports 16 and `itemsize` reports 2. Multi-byte formats use native byte order, so `cast` is not a wire-format decoder.
  • Why can't you append to a bytearray while a memoryview of it exists?
    Appending may reallocate the buffer, which would leave the view pointing at freed memory. CPython therefore counts outstanding exports and raises `BufferError: Existing exports of data: object cannot be re-sized` for any length-changing operation. Writing to existing positions is still allowed. Release the view — explicitly or by using it as a context manager — and the resize succeeds.
  • Is a memoryview always faster than slicing?
    No. It avoids the copy, not the interpreter. Per-item Python access through a view is no faster than through bytes, and for small regions the view's own allocation and release bookkeeping outweigh a memcpy of a few dozen bytes. The win appears on large buffers, on repeated subslicing, and when handing a region to code that can consume foreign memory directly.

Slicing bytes is photocopying a page from a book; a memoryview is a bookmark plus a ruler laid across the original page. The bookmark is cheap, but the book has to stay on the desk.

saying these in an interview costs you the question

  • Thinks memoryview copies the data it wraps
  • Expects len() to report bytes after a cast
  • Believes a view of a bytes object is writable
  • Never releases views, then calls BufferError a bug
  • Confuses memoryview with an I/O buffer object
  • Assumes a view makes per-item Python loops fast

context