Why does assigning into memoryview(b'abc') raise TypeError while memoryview(bytearray(b'abc')) allows it?
answer
- The view does not decide this
- Immutable source, immutable window
- There is a boolean attribute to check
- Writes go straight into the exporter
- toreadonly gives a look without a licence
basics
~20 sA 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 sWritability 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 linesfrozen = 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
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.
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.
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.
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