skip to content

Why does assigning into memoryview(b'abc') raise TypeError while memoryview(bytearray(b'abc')) allows it?

level: middleimportance: should knowfreq 40%

answer

  1. The view does not decide this
  2. Immutable source, immutable window
  3. There is a boolean attribute to check
  4. Writes go straight into the exporter
  5. toreadonly gives a look without a licence

basics

~20 s

A bytes object is immutable, so the buffer it exports is flagged read-only and the view refuses writes with TypeError: cannot modify read-only memory. A bytearray exports writable memory, so its view can be written through into the original.

solid answer

~50 s

Writability is a property of the *exporter*, not of the view. `bytes` exports a read-only buffer, so `memoryview(b'abc')[0] = 0x50` raises `TypeError: cannot modify read-only memory`; `bytearray` and `array.array` export writable buffers, so the same assignment succeeds and the change is visible in the original object. Check before you write with the `readonly` attribute rather than catching the exception. Two details bite people: a default `'B'`-format view takes an `int` on element assignment, not a one-byte `bytes`, and slice assignment must be exactly the same length as the region it replaces — a mismatch raises `ValueError: memoryview assignment: lvalue and rvalue have different structures`, because a view describes a fixed window and cannot grow it. `view.toreadonly()` gives you a read-only view over writable memory, which is how you hand a caller a look without a licence to write.

code

python · 11 lines
python
frozen = memoryview(b"payload")
print(frozen.readonly)          # True
try:
    frozen[0] = 0x50
except TypeError as exc:
    print(exc)                  # cannot modify read-only memory

live = memoryview(bytearray(b"payload"))
print(live.readonly)            # False
live[0] = 0x50
print(live.obj)                 # bytearray(b'Payload')

go deeper

for a junior

Recall that bytes is immutable and bytearray is not, and that a view over each inherits that. Knowing the readonly attribute exists and that writes go into the original object is enough here.

for a middle

Explain that the exporter sets the flag at export time, that element assignment takes an int under format 'B', and that slice assignment must match length exactly because a view cannot resize storage.

for a senior

Show you check readonly at the API boundary instead of catching TypeError mid-loop, and that you use toreadonly() to share buffers without granting mutation. Be clear it is a contract, not a security boundary.

for a principal

Frame it as an ownership decision: whether internal buffers escape as writable views, read-only views or copies determines whether callers can corrupt your state, and that policy belongs in the API contract rather than in each function.

## Writability belongs to the exporter A `memoryview` never decides whether its memory can be written. It reports what the exporting object declared when it handed out the buffer. Immutable exporters — `bytes`, and anything else whose contents are fixed for its lifetime — set the read-only flag, and the view enforces it: ```python frozen = memoryview(b"payload") print(frozen.readonly) # True frozen[0] = 0x50 # TypeError: cannot modify read-only memory ``` Mutable exporters — `bytearray`, `array.array`, `mmap.mmap` and writable C-level buffers — export writable memory: ```python live = memoryview(bytearray(b"payload")) print(live.readonly) # False live[0] = 0x50 print(live.obj) # bytearray(b'Payload') ``` Note the last line: `view.obj` is the exporting object, and the write went straight into it. There is no commit step and no copy back. That is the point of a writable view, and it is also the reason a function that accepts one is a function that can mutate its caller's data. ## Check the flag, do not catch the error `readonly` is a plain boolean attribute, so a function that intends to write should assert on it up front rather than discovering the problem halfway through a partial write: ```python def fill(target: memoryview, value: int) -> None: if target.readonly: raise ValueError("fill() needs a writable buffer") for i in range(len(target)): target[i] = value ``` Failing at the boundary is much easier to debug than a `TypeError` raised from element 4,000 of a loop that has already modified elements 0 to 3,999. ## Two assignment rules that trip people up **Element assignment takes an int.** A default view over bytes-like memory has format `'B'` — unsigned bytes — so its elements *are* integers: ```python view = memoryview(bytearray(b"abcd")) view[0] = 120 # fine view[0] = b"x" # TypeError: memoryview: invalid type for format 'B' ``` This surprises people who expect a view to behave like a `bytearray`, where `buf[0]` reads as an `int` but slice assignment takes bytes. On a view, the element type follows the format string. **Slice assignment must match length exactly.** A view describes a fixed window; it has no mechanism to make the underlying storage bigger or smaller: ```python view = memoryview(bytearray(b"abcd")) view[0:2] = b"XY" # fine view[0:2] = b"xyz" # ValueError: memoryview assignment: # lvalue and rvalue have different structures ``` On a `bytearray` itself, `buf[0:2] = b"xyz"` would happily grow the object. Through a view it cannot, because growing means reallocating storage that other exports may be pointing at. The same reasoning is why an exported `bytearray` refuses `append` and `extend` outright. ## Handing out a view you do not want written The interesting case is memory that *is* writable but that you do not want a particular caller to touch. Wrapping it in `bytes()` would copy, defeating the purpose. `toreadonly()`, added in 3.8, produces a second view over the same memory with the read-only flag set: ```python buf = bytearray(b"secret-ish") public = memoryview(buf).toreadonly() print(public.readonly) # True print(bytes(public)) # b'secret-ish' - reading is fine ``` The caller can read and slice for free and cannot write. This is the right shape for a parsing API that hands regions of an internal buffer to plugin code: zero-copy in, no mutation out. It is not a security boundary — the caller can still reach `public.obj` — but it is a clear, cheap contract. ## Read-only does not mean unchanging A subtlety worth having ready: a read-only view over a `bytearray` guarantees that *you* cannot write through the view, not that the bytes will hold still. The owner of the `bytearray` can keep writing to it by other means, and those writes are visible through your read-only view immediately, because there is only one copy of the memory. If you need a stable snapshot, you need an actual copy — `tobytes()` — and at that point you have chosen correctness over zero-copy, which is a perfectly reasonable trade to make explicitly. The short version: `readonly` mirrors the exporter's mutability, element writes take integers under the `'B'` format, slice writes must be the same length, and `toreadonly()` is how you share writable memory without sharing the right to write it.

  • How do you give a caller zero-copy read access to a buffer you keep writing to?
    Hand out `memoryview(buf).toreadonly()`. It shares the same memory, so there is no copy, but the read-only flag makes any write through it a `TypeError`. Be honest about what it is not: the caller still sees your later writes, because there is only one copy of the bytes, and `view.obj` still reaches the writable exporter. For a stable snapshot you must actually copy with `tobytes()`.
  • Why does view[0] = b'x' fail on a view where view[0:1] = b'x' succeeds?
    Element access follows the view's format string. A default view has format `'B'`, so a single element is an unsigned byte represented as an `int`; assigning `b'x'` raises `TypeError: memoryview: invalid type for format 'B'`. A slice, by contrast, is a region and takes a bytes-like object of exactly matching length, so `view[0:1] = b'x'` is the correct form.
  • Can a memoryview over a bytes object ever become writable?
    No. Writability is decided by the exporter at export time and cannot be upgraded — there is no `towritable()`. `bytes` is immutable, so every view over it reports `readonly` as True. If you need to edit the contents, copy into a `bytearray` first and take a view of that; the copy is the price of mutability.

saying these in an interview costs you the question

  • Thinks memoryview itself chooses read-only or writable
  • Expects view[0] = b'x' to work on a default view
  • Assumes slice assignment can grow the underlying buffer
  • Says bytes(view) is the way to deny writes cheaply
  • Believes a read-only view guarantees the bytes never change

context