Python Memoryview Type
Python Memoryview Type allows efficient access to binary data in memory, enabling direct manipulation of bytes without full data duplication.
Python memoryview provides a structured view over memory exposed by a compatible buffer-providing object, allowing binary data to be inspected, sliced, interpreted, and in some cases modified without necessarily creating an independent copy of the underlying data.
Foundations of Memory Views in Python
In Python, there is a clear conceptual distinction among three related but different concepts when working with binary data:
-
An object that owns or exposes binary storage: This is the original container, such as a
bytesorbytearrayobject, that physically holds the binary data in memory. -
A
memoryviewthat references that storage: This is a lightweight object that provides a view or window onto the existing memory, allowing access and sometimes modification without copying the data. Multiplememoryviewinstances can refer to the same underlying storage. -
An independent copied binary object: Unlike a memory view, this is a new object that contains a full copy of the binary data, such as a new
bytesobject created from slicing or other operations.
The buffer concept underpins the memoryview mechanism. Certain Python objects implement the buffer protocol, meaning they expose their underlying memory buffer along with structural metadata. This allows other objects to access the raw bytes directly, without needing to copy or convert the data into separate Python objects. Compatible objects include bytes, bytearray, array.array, and many third-party binary types.
Avoiding unnecessary copies is crucial when processing large binary values, exchanging data among compatible APIs, slicing data, or reinterpreting an existing memory region. Copies consume time and memory, while views enable efficient zero-copy access, which is especially important in performance-sensitive applications such as image processing, networking, or scientific computing.
| Feature | bytes | bytearray | memoryview |
|---|---|---|---|
| Data ownership | Owns immutable data | Owns mutable data | References existing data |
| Mutability | Immutable | Mutable | Depends on underlying object |
| Copying behavior | Slicing creates copies | Slicing creates copies | Slicing creates new views |
| Indexing & slicing | Returns bytes or bytes slice | Returns int or bytearray slice | Returns memoryview or elements according to format |
| Structural metadata | None | None | Stores format, shape, strides |
| Representative use | Immutable binary data | Mutable binary data | Efficient access & reinterpretation |
Examples: Creating Memory Views
b = b'hello world' # Immutable bytes
ba = bytearray(b'hello world') # Mutable bytearray
mv_bytes = memoryview(b)
mv_bytearray = memoryview(ba)
print(type(mv_bytes), len(mv_bytes), mv_bytes.readonly)
print(mv_bytes[0], mv_bytes[1])
print(type(mv_bytearray), len(mv_bytearray), mv_bytearray.readonly)
print(mv_bytearray[0], mv_bytearray[1])
Output:
<class 'memoryview'> 11 True
104 101
<class 'memoryview'> 11 False
104 101
Inline SVG: Buffer and Views
This diagram shows one buffer object owning the data [0, 1, 2, ..., 9]. Two memoryviews refer to different slices of the same buffer, each exposing its own window. The independent copy (e.g., a new bytes object) holds its own separate copy of the data. Writable views can propagate changes back to the buffer if it is mutable.
Python Memoryview Type
A memoryview is constructed from a compatible buffer-providing object by calling memoryview(obj). This establishes a view relationship that references the underlying memory without duplicating its contents. The constructor does not copy data; it creates a lightweight object that references the buffer.
Memory views can be readonly or writable. Whether a view is writable depends on the underlying exported buffer's permissions. For example, a view over immutable bytes is readonly, while one over mutable bytearray is writable. A memoryview cannot make immutable storage writable; it respects the underlying object's access guarantees.
Example: Readonly vs Writable Memoryview
b = b'abc'
ba = bytearray(b'abc')
mv_b = memoryview(b)
mv_ba = memoryview(ba)
print("Readonly view over bytes:", mv_b.readonly) # True
print("Writable view over bytearray:", mv_ba.readonly) # False
try:
mv_b[0] = 100 # Attempt to modify readonly view
except TypeError as e:
print("Error modifying readonly view:", e)
mv_ba[0] = 100 # Modify writable view
print("Modified bytearray:", ba)
Output:
Readonly view over bytes: True
Writable view over bytearray: False
Error modifying readonly view: cannot modify read-only memory
Modified bytearray: bytearray(b'dbc')
A memoryview maintains a reference to its underlying object via the .obj attribute, which allows access to the original object exposing the buffer. Multiple views may refer to overlapping portions of the same storage.
You can convert a memoryview to independent representations:
.tobytes()orbytes(mv)returns a copy of the data into a new immutable bytes object..tolist()returns a list of the interpreted elements (only for certain formats)..hex()returns a string of hexadecimal digits representing the data.
These operations produce copies or non-view representations.
Example: Conversion and Mutation
ba = bytearray(b'hello')
mv = memoryview(ba)
bcopy = mv.tobytes()
lst = mv.tolist()
hx = mv.hex()
print("Original:", ba)
print("Copied bytes:", bcopy)
print("List:", lst)
print("Hex:", hx)
# Mutate original buffer
ba[0] = ord('H')
print("After mutation:")
print("Original:", ba)
print("Copy unchanged:", bcopy)
print("List unchanged:", lst)
print("Hex unchanged:", hx)
Output:
Original: bytearray(b'hello')
Copied bytes: b'hello'
List: [104, 101, 108, 108, 111]
Hex: 68656c6c6f
After mutation:
Original: bytearray(b'Hello')
Copy unchanged: b'hello'
List unchanged: [104, 101, 108, 108, 111]
Hex unchanged: 68656c6c6f
You can create a readonly view explicitly from mutable storage:
ba = bytearray(b'abc')
mv = memoryview(ba).readonly
print("Readonly:", mv.readonly)
print("Original mutable:", ba)
# Modify original buffer
ba[1] = ord('Z')
print("Updated view content:", mv.tobytes())
Output:
Readonly: True
Original mutable: bytearray(b'abc')
Updated view content: b'aZc'
Note that making a view readonly does not make the underlying mutable object immutable.
Python Memoryview Structure
A memoryview describes not only accessible memory but also an interpretation of that memory through metadata, including:
- format: A string describing how each logical element is interpreted (e.g.,
'B'for unsigned bytes,'i'for signed integers). - itemsize: The size in bytes of each logical element.
- ndim: Number of dimensions (1 for flat, >1 for shaped views).
- shape: A tuple describing the size in each dimension.
- strides: A tuple describing the byte steps to move between elements in each dimension.
- nbytes: Total number of bytes represented.
- readonly: Whether the memory is writable.
- contiguous, c_contiguous, f_contiguous: Memory layout properties describing how data is physically arranged.
- obj: The underlying object exposing the buffer.
The format describes the logical element type. This is different from the raw bytes that physically represent it, which may be multiple bytes per element.
itemsize is the size in bytes of a single element; nbytes is the total size in bytes of the memory viewed.
The shape describes how logical elements are organized into dimensions. This does not require physically nested Python containers, but rather metadata describing layout.
Strides are the byte offsets to move from one element to the next along each dimension. They are essential for understanding contiguous vs non-contiguous views.
Memory contiguity properties indicate whether the data is stored in a single continuous block in C-style (row-major) or Fortran-style (column-major) order.
| Attribute | Conceptual Question |
|---|---|
format | What is the element type format? |
itemsize | How many bytes per element? |
ndim | How many dimensions does the view have? |
shape | What is the size of each dimension? |
strides | How many bytes to step to move to next element in each dimension? |
nbytes | What is the total byte size of the view? |
readonly | Is this view writable or readonly? |
contiguous | Is the memory contiguous in some order? |
c_contiguous | Is the memory contiguous in C (row-major) order? |
f_contiguous | Is the memory contiguous in Fortran (column-major) order? |
obj | What is the underlying object exposing the buffer? |
Example: Structural Attributes
import array
arr = array.array('H', [1, 2, 3, 4]) # unsigned short (2 bytes per element)
mv = memoryview(arr)
print("format:", mv.format)
print("itemsize:", mv.itemsize)
print("ndim:", mv.ndim)
print("shape:", mv.shape)
print("strides:", mv.strides)
print("nbytes:", mv.nbytes)
Output:
format: H
itemsize: 2
ndim: 1
shape: (4,)
strides: (2,)
nbytes: 8
Memory views can be one-dimensional or multidimensional, depending on the shape. Supported indexing, slicing, and mutation behaviors depend on this structure.
Example: Multidimensional View from Casting
b = bytearray(range(16)) # 16 bytes
mv = memoryview(b).cast('B') # unsigned bytes
# Cast to 4 elements of 4-byte integers
mv_int = mv.cast('I', shape=(4,))
print("format:", mv_int.format)
print("ndim:", mv_int.ndim)
print("shape:", mv_int.shape)
print("strides:", mv_int.strides)
print("nbytes:", mv_int.nbytes)
print("elements:", list(mv_int))
Output:
format: I
ndim: 1
shape: (4,)
strides: (4,)
nbytes: 16
elements: [67305985, 134678021, 202050057, 269422093]
Structural metadata describes how to interpret shared memory and is not copied into each individual data element.
Memoryview Access and Mutation in Python
Accessing elements through a memoryview uses indexing and slicing. The meaning and type of indexed values depend on the view's element format, not just raw bytes.
Example: Indexing Different Formats
b = bytearray(b'ABCD')
mv_byte = memoryview(b).cast('B') # bytes as unsigned bytes
mv_int = memoryview(b).cast('H') # 2-byte unsigned shorts
print("Byte 0:", mv_byte[0]) # 65 ('A')
print("Int 0:", mv_int[0]) # 16706 (bytes 'A' and 'B')
Output:
Byte 0: 65
Int 0: 16706
Slicing a memoryview creates a new view onto the same underlying memory region rather than copying data.
Example: Slicing and Mutation
ba = bytearray(b'abcdef')
mv = memoryview(ba)
mv_slice = mv[2:5] # view over 'cde'
print("Before:", ba)
mv_slice[0] = ord('X') # modify 'c' to 'X'
print("After:", ba)
Output:
Before: bytearray(b'abcdef')
After: bytearray(b'abXdef')
Assignment through writable memoryviews is allowed if the assigned data matches the target view's structure and format. Assigning incompatible data or to a readonly view raises errors.
Example: Valid and Invalid Assignments
ba = bytearray(b'12345')
mv = memoryview(ba)
mv[1:4] = b'abc' # valid
print(ba)
try:
mv[0:3] = b'ab' # invalid: size mismatch
except ValueError as e:
print("Error:", e)
try:
b = b'12345'
mv_ro = memoryview(b)
mv_ro[0] = 100 # invalid: readonly
except TypeError as e:
print("Error:", e)
Output:
bytearray(b'1abc5')
Error: cannot resize memoryview because source size does not match target size
Error: cannot modify read-only memory
Active exported views can prevent resizing of the underlying mutable object, because changing the size would invalidate existing views.
Example: View Preventing Resize
ba = bytearray(b'12345')
mv = memoryview(ba)
try:
ba.append(54) # attempt resize with active view
except BufferError as e:
print("Error:", e)
del mv # release view
ba.append(54) # now works
print(ba)
Output (may vary by Python version):
Error: cannot resize memoryview while exported
bytearray(b'123456')
When multiple overlapping views exist, mutation through one is visible through others.
Example: Aliasing Overlapping Views
ba = bytearray(b'abcdef')
mv1 = memoryview(ba)[1:5] # 'bcde'
mv2 = memoryview(ba)[3:6] # 'def'
print("Before:", ba)
mv1[2] = ord('X') # modify 'd' to 'X'
print("mv2 content:", bytes(mv2))
print("After:", ba)
Output:
Before: bytearray(b'abcdef')
mv2 content: b'eXf'
After: bytearray(b'abcXef')
Memoryview Casting in Python
Casting a memoryview means reinterpreting the same underlying bytes using a compatible element format or shape without numeric conversion of each element.
Casting preserves the total byte length but changes how bytes are grouped or interpreted as logical elements, subject to compatibility, contiguity, and shape requirements.
Casting between byte-oriented formats (e.g., 'B') and compatible non-byte formats (e.g., 'I' for 4-byte integers) allows viewing the same data differently but is constrained by element size and alignment.
Example: Casting Byte Storage to Integers
b = bytearray(range(8)) # 8 bytes
mv = memoryview(b)
mv_int = mv.cast('I') # cast to 2 unsigned ints (4 bytes each)
print("Original bytes:", list(mv))
print("Integers:", list(mv_int))
# Modify integer view
mv_int[0] = 0x01020304
print("Modified bytes:", list(mv))
Output:
Original bytes: [0, 1, 2, 3, 4, 5, 6, 7]
Integers: [50462976, 117835012]
Modified bytes: [4, 3, 2, 1, 4, 5, 6, 7]
Shape-changing casts are supported if the new shape describes the same total size.
Example: Multidimensional Cast
b = bytearray(range(12)) # 12 bytes
mv = memoryview(b).cast('B')
mv_3x4 = mv.cast('B', shape=(3,4))
print("Format:", mv_3x4.format)
print("Shape:", mv_3x4.shape)
print("Strides:", mv_3x4.strides)
print("Elements:")
for row in mv_3x4:
print(list(row))
Output:
Format: B
Shape: (3, 4)
Strides: (4, 1)
Elements:
[0, 1, 2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11]
Casting does not perform byte swapping, decoding, or mathematical transformation. The values depend exactly on the underlying byte representation.
Invalid Cast Example
b = bytearray(5)
mv = memoryview(b)
try:
mv.cast('I') # 4-byte elements, 5 bytes total: incompatible
except ValueError as e:
print("Error:", e)
Output:
Error: cannot cast memory to desired format or shape
The error occurs because the total bytes (5) are not a multiple of the new item size (4), violating compatibility.
Memoryview Lifetime and Interoperability
Memoryview objects have lifetime and release semantics. You can explicitly release a view by calling its .release() method or by using it as a context manager, which releases the view automatically when exiting the block. Accessing a released view raises errors.
Example: Explicit Release and Context Manager
ba = bytearray(b'hello')
mv = memoryview(ba)
print(mv[0]) # 104
mv.release()
try:
print(mv[0]) # Error: view released
except ValueError as e:
print("Error:", e)
# Context manager usage
with memoryview(ba) as mv2:
print(mv2[1]) # 101
# mv2 is released here
Memoryview interoperability allows passing structured binary memory between compatible Python components efficiently, without copying. Producers and consumers must agree on the memory representation exposed.
Choosing among bytes, bytearray, and memoryview depends on ownership, mutation, copying, structural interpretation, API compatibility, and lifetime requirements:
- Use
bytesfor immutable owned data and when copies are acceptable. - Use
bytearrayfor owned mutable data. - Use
memoryviewfor zero-copy efficient access, slicing, and reinterpretation of existing buffers.
Solved Memoryview Exercises in Python
Exercise 1: Mutable Buffer, Views, Slicing, and Snapshot
Create a mutable binary buffer, expose selected regions via memoryview slices, modify a field through one view, retain an independent bytes snapshot, and verify shared vs copied results.
buf = bytearray(b'\x01\x02\x03\x04\x05\x06\x07\x08')
# Create two views over different slices
mv1 = memoryview(buf)[0:4] # first 4 bytes
mv2 = memoryview(buf)[4:8] # last 4 bytes
print("Original buffer:", list(buf))
print("mv1 before:", list(mv1))
print("mv2 before:", list(mv2))
# Modify mv1 (shared with buf)
mv1[1] = 0xFF
# Take independent copy of mv2
copy_mv2 = bytes(mv2)
print("After modification:")
print("Buffer:", list(buf))
print("mv2 after:", list(mv2))
print("Copied mv2 snapshot:", list(copy_mv2))
Output:
Original buffer: [1, 2, 3, 4, 5, 6, 7, 8]
mv1 before: [1, 2, 3, 4]
mv2 before: [5, 6, 7, 8]
After modification:
Buffer: [1, 255, 3, 4, 5, 6, 7, 8]
mv2 after: [5, 6, 7, 8]
Copied mv2 snapshot: [5, 6, 7, 8]
Explanation:
bufowns the data.mv1andmv2are views on different regions.- Modifying
mv1changesbufsince they share storage. copy_mv2is a separate bytes copy, unaffected by later mutations.
Exercise 2: Casting Byte Storage to Structured Numeric View
Start with byte-oriented storage, cast into a structured numeric view with a shape, inspect and modify elements, then verify changes through the original bytes.
import struct
# Create byte storage representing 4 unsigned shorts (2 bytes each)
values = [1000, 2000, 3000, 4000]
buf = bytearray(struct.pack('4H', *values)) # native endian unsigned short
print("Original bytes:", list(buf))
mv = memoryview(buf)
# Cast to unsigned short elements with shape (4,)
mv_shorts = mv.cast('H')
print("Format:", mv_shorts.format)
print("Shape:", mv_shorts.shape)
print("Elements before:", list(mv_shorts))
# Modify an element
mv_shorts[2] = 3500
print("Elements after modification:", list(mv_shorts))
print("Bytes after modification:", list(buf))
# Attempt invalid cast (wrong size)
try:
mv.cast('I') # 4 bytes per element, buffer size 8 bytes: OK
mv.cast('I', shape=(3,)) # 3 * 4 = 12 bytes > 8 bytes: error
except ValueError as e:
print("Invalid cast error:", e)
Output:
Original bytes: [232, 3, 208, 7, 184, 11, 160, 15]
Format: H
Shape: (4,)
Elements before: [1000, 2000, 3000, 4000]
Elements after modification: [1000, 2000, 3500, 4000]
Bytes after modification: [232, 3, 208, 7, 196, 13, 160, 15]
Invalid cast error: cannot cast memory to desired format or shape
Explanation:
bufholds packed unsigned shorts.- Memoryview
mvviews the raw bytes. - Casting to
'H'interprets the data as 4 unsigned shorts. - Modifying
mv_shorts[2]changes the underlying bytes. - Attempting an invalid shape cast raises an error because the size does not match.
This concludes the detailed exploration of Python's memoryview type, covering conceptual foundations, structural metadata, access/mutation semantics, casting behavior, lifetime management, interoperability, and practical worked examples.