Why does slicing a memoryview of a bytearray avoid the copy that slicing the bytearray itself makes?
answer
- Two ways to take part of a buffer
- One allocates, the other points
- Cost does not grow with slice length
- The view records offset, length, format
- bytes() or tobytes() is where copying happens
basics
~20 sSlicing a bytearray allocates a second bytearray and copies the bytes. A memoryview slice only records an offset and length into the same storage, so it costs the same tiny amount whatever the slice size.
solid answer
~40 s`buf[10:900_000]` allocates a new `bytearray` and copies nearly a megabyte; `memoryview(buf)[10:900_000]` allocates a small view object that points into `buf`'s existing storage, so the slice is O(1) instead of O(n). The view exposes that same memory through the buffer protocol, so `len()`, indexing, comparison and further slicing all work without another copy, and writing through a writable view mutates the original `bytearray`. The copy comes back exactly where you ask for it: `bytes(view)` or `view.tobytes()` materialises the region, and that is the one place to pay for it — at the boundary where an API demands real `bytes`. The payoff is parsing or forwarding a large payload in constant extra memory rather than doubling it per slice. The cost is lifetime coupling: while a view is alive the exporting `bytearray` cannot be resized.
code
python · 9 linesimport sys
payload = bytearray(1_000_000)
copy = payload[100:900_000]
view = memoryview(payload)[100:900_000]
print(sys.getsizeof(copy)) # 899957 - a real copy
print(sys.getsizeof(view)) # 184 - just a descriptor
print(view.nbytes) # 899900 - bytes describedgo deeper
Be ready to say plainly that a bytearray slice copies and a memoryview slice does not, and to name tobytes() as the place the copy happens. Knowing memoryview exists and what problem it solves is the bar here.
Explain the mechanics: the buffer protocol export, why the slice is O(1), that a default view indexes to ints and compares to bytes by value, and that a writable view mutates the exporter in place.
Demonstrate judgment about when a view is worth it — large regions, repeated slicing, recv_into-style fills — and show you know the two costs: the view pins the exporter alive, and it blocks resizing until released.
Own the tradeoff at API-design level: whether your library's parse boundary hands out views or copies decides whether callers can leak pinned buffers, and a documented copy at the edge is often worth more than the memory it saves.
## Two operations that look identical and are not `buf[10:20]` on a `bytearray` calls its slice `__getitem__`, which allocates a brand-new `bytearray` of the requested length and copies the bytes into it. The cost is proportional to the slice length and the result is an independent object: later writes to `buf` are invisible to it. `memoryview(buf)[10:20]` allocates a small, fixed-size view object that records the exporting object, a pointer into its storage, a shape and a stride. The cost is constant whatever the slice length, and the result *aliases* `buf`. That is the whole idea of `memoryview`: it is a Python-level handle on another object's memory, not a container of its own. ## The buffer protocol underneath The buffer protocol is a C-level contract by which an object that owns a contiguous block of memory can hand out a description of that block — base pointer, item size, format string, shape, strides, and a read-only flag — instead of handing out a copy. `bytes`, `bytearray`, `array.array`, `mmap.mmap` and many C extension types implement it. `memoryview` is the built-in that wraps such an export and makes it usable from Python. Since 3.12 (PEP 688) the protocol is reachable from Python too: `__buffer__` and `__release_buffer__` are real dunders a Python class can define, and `collections.abc.Buffer` is the abstract base you annotate against when a parameter means "anything `memoryview` accepts". ## Measuring the difference ```python import sys payload = bytearray(1_000_000) copy = payload[100:900_000] # a real bytearray, ~900 KB view = memoryview(payload)[100:900_000] # a view, ~184 bytes print(sys.getsizeof(copy)) # 899957 print(sys.getsizeof(view)) # 184 print(view.nbytes) # 899900 -- bytes described, not owned ``` `sys.getsizeof(view)` reports the view object's own footprint. `view.nbytes` reports how much memory it *describes*. The gap between those two numbers is exactly the copy you did not make. ## Reading and writing through a view A default view over a bytes-like object has format `'B'`, so indexing yields an `int`, not a one-byte `bytes`: ```python view = memoryview(b"INFO") print(view[0]) # 73 print(view == b"INFO") # True -- compares by value print(view.tolist()) # [73, 78, 70, 79] print(view.tobytes()) # b'INFO' ``` Comparison against a bytes-like object works by value, which means a parser can often match on a view directly and never materialise anything. When you do need real `bytes`, `bytes(view)` and `view.tobytes()` both copy exactly the described region. If the exporter is mutable, the view is writable, and assignment travels straight back into the original storage: ```python buf = bytearray(b"level=INFO msg=ok") view = memoryview(buf) view[6:10] = b"WARN" print(buf) # bytearray(b'level=WARN msg=ok') view.release() ``` Slice assignment through a view must be the same length as the region it replaces — the view describes a fixed window and cannot grow it. ## What sharing costs you Aliasing has two consequences worth stating out loud in an interview. First, the view keeps the exporter alive: `view.obj` is a strong reference to the underlying `bytearray`, so holding one small view over a huge buffer pins the whole buffer. If you slice a 100 MB payload down to a 40-byte field and stash it, you have pinned 100 MB; `tobytes()` on that field is the fix. Second, the exporter cannot be *resized* while an export exists. `append`, `extend`, `+=`, `del`, `clear` and any length-changing slice assignment on an exported `bytearray` raise `BufferError`. In-place same-length writes are still fine. This is why `memoryview` is a context manager: `with memoryview(buf) as m:` releases the export at the end of the block, and `m.release()` does it explicitly. ## When not to reach for it A view is not a general speed-up. Per-element access *through* a view is, if anything, marginally slower than indexing the bytes object directly — the win is avoiding the copy, not faster element access. For a 20-byte header, the copy is free and a view is just an extra object plus a lifetime obligation. `memoryview` earns its keep when the region is large, when you slice repeatedly, when you want to write into an existing buffer rather than concatenate new ones, or when you must hand a region to something that reads through the buffer protocol — `socket.socket.recv_into`, a `readinto` on a binary stream, `hashlib` digests, `struct.unpack_from` — without an intermediate copy. The short version: `bytes`-like slicing is simple and copies; `memoryview` slicing is cheap and shares. Choose the copy for small pieces you want to own, the view for large regions you only want to look at.
- How do you turn a memoryview back into bytes, and what does that cost?`bytes(view)` and `view.tobytes()` both allocate and copy exactly the region the view describes — `view.nbytes` of it. That is the deliberate boundary: keep views while you are locating and comparing, and copy once at the point where an API insists on a real `bytes` object, or where you want to stop pinning a large buffer.
- Does holding a memoryview keep the original bytearray alive?Yes. `view.obj` is a strong reference to the exporting object, so a 40-byte view over a 100 MB `bytearray` pins the whole 100 MB. If you are retaining a small field extracted from a large payload, call `tobytes()` and drop the view, otherwise you have built a memory leak that looks like a small object.
- What happens if you slice a memoryview with a step, like view[::2]?You still get a zero-copy view, but a non-contiguous one: `c_contiguous` becomes `False`, `nbytes` counts only the selected elements rather than the span they cover, and `cast()` refuses it because casts are restricted to C-contiguous views. `tobytes()` still works and gathers the selected bytes into a fresh copy.
A bytearray slice is a photocopy of pages 10 to 20; a memoryview slice is a bookmark pair marking those pages in the original book. The bookmark is cheap and always current, but the book cannot be re-bound while it is in place.
saying these in an interview costs you the question
- Says memoryview copies the data lazily on first read
- Thinks bytes(view) is also zero-copy
- Believes slicing a bytearray already returns a view
- Claims indexing through a view is faster than indexing bytes
- Forgets the view pins the whole exporting object alive
- Assumes a view keeps working after the source is resized