Python Basics: Share Binary Buffers with memoryview
A binary parser often needs a small region of a larger buffer. A memoryview lets that region refer to the existing storage rather than copying its bytes. That can also expose shared changes, so ownership and lifetime matter.
These examples use tiny bytearrays to make the behavior visible. They are not a performance benchmark, and none reads files or contacts a service.
Prepare a scratch directory
These examples were tested on Linux with Python 3.12.14. These examples target Python 3.8 or newer, including toreadonly. Use Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Run without -O so the assertions remain enabled.
Create a fresh directory and keep using the same terminal:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1If either setup command fails, stop before saving any files. Save each named source below as UTF-8 in the new directory.
1. Update a region without replacing the buffer
Save as edit_buffer_region.py:
frame = bytearray(b"ID:cat")
with memoryview(frame) as whole:
with whole[3:] as payload:
assert payload.obj is frame
assert payload.nbytes == 3
payload[:] = b"dog"
assert frame == bytearray(b"ID:dog")
print("Shared owner:", payload.obj is frame)
print("Payload:", payload.tobytes().decode("ascii"))
try:
payload[:] = b"longer"
except ValueError:
print("Rejected: replacement has a different size")
else:
raise AssertionError("The view unexpectedly resized")
print("Frame:", frame.decode("ascii"))Run it:
python3 -B edit_buffer_region.pyOutput:
Shared owner: True
Payload: dog
Rejected: replacement has a different size
Frame: ID:dogThe slice shares the bytearray’s storage. Assigning three bytes updates the original frame, while a longer replacement is rejected. The tobytes call deliberately creates a copy for display; creating the view did not require that copy. See the memoryview reference.
2. Distinguish read-only access from an immutable snapshot
Save as inspect_readonly_view.py:
buffer = bytearray(b"OPEN")
with memoryview(buffer) as writable:
with writable.toreadonly() as readonly:
snapshot = readonly.tobytes()
assert readonly.readonly
try:
readonly[0] = ord("X")
except TypeError:
print("Read-only assignment: blocked")
else:
raise AssertionError("The read-only view accepted a write")
buffer[:] = b"DONE"
assert readonly.tobytes() == b"DONE"
assert snapshot == b"OPEN"
print("Live view:", readonly.tobytes().decode("ascii"))
print("Earlier snapshot:", snapshot.decode("ascii"))Run it:
python3 -B inspect_readonly_view.pyOutput:
Read-only assignment: blocked
Live view: DONE
Earlier snapshot: OPENRead-only prevents writes through that view. It does not prevent the owner, or another writable view, from changing the same memory. Take an explicit bytes copy when later code needs the value as it was at a particular point. toreadonly changes access, not storage ownership.
3. Release a view before resizing its owner
Save as release_buffer_view.py:
buffer = bytearray(b"AB")
with memoryview(buffer) as view:
try:
buffer.extend(b"C")
except BufferError:
print("Resize while viewed: blocked")
else:
raise AssertionError("The bytearray resized with an active view")
assert view.tobytes() == b"AB"
buffer.extend(b"C")
assert buffer == bytearray(b"ABC")
print("After release:", buffer.decode("ascii"))
try:
view[0]
except ValueError:
print("Released view: unavailable")
else:
raise AssertionError("The released view still allowed access")Run it:
python3 -B release_buffer_view.pyOutput:
Resize while viewed: blocked
After release: ABC
Released view: unavailableAn active view temporarily prevents this bytearray from resizing. Leaving with releases the view, after which extending the owner succeeds. Accessing the released view fails. Release every outstanding view before expecting the restriction to disappear. The release method documents this lifetime rule.
Keep the scope of borrowed buffers small. These fixtures use one-dimensional byte views; other buffer providers can have different item sizes, formats, shapes, or contiguity. A memoryview is not a portable binary file format, and avoiding a copy does not make concurrent mutation safe.
Clean up
In the same terminal, remove only these sources and the empty scratch directory:
rm -- edit_buffer_region.py inspect_readonly_view.py release_buffer_view.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf rmdir reports remaining files, inspect them instead of deleting unknown contents.
References
Acknowledgements
This post was created with assistance from AI.