Back to Blog
Python

Python Bytes Slicing: Syntax and Behavior

python bytes slicing: Learn how to slice bytes objects in Python: syntax, step, negative indices, copy behavior, and practical binary data extraction.

bytesslicingbinary datamemoryviewbytearray
Diagram showing a bytes object sliced into segments with indices and step, illustrating Python bytes slicing.

Python bytes slicing is a core operation when working with binary data, but its behavior differs from string slicing in one critical way: every slice returns a new bytes object. Understanding the exact syntax and the copy semantics helps you avoid subtle bugs and write efficient code for parsing protocols, file formats, or network packets.

Basic Bytes Slicing Syntax

A bytes object is an immutable sequence of integers in the range 0 to 255. Slicing uses the same [start:stop:step] notation as lists and strings. The start index is inclusive, stop is exclusive, and step defaults to 1.

data = b'abcdef' print(data[1:4]) # b'bcd' print(data[:3]) # b'abc' print(data[3:]) # b'def' print(data[:]) # b'abcdef'

When start or stop are omitted, Python uses the beginning or end of the sequence. A full slice data[:] returns a complete copy of the original bytes object, not the same object. This is important when you want to prevent external code from holding a reference to a larger buffer.

Negative Indices and Step Values

Negative indices count from the end of the bytes object. -1 refers to the last byte, -2 to the second last, and so on. The step parameter allows you to skip bytes, which is useful for decimation or reversing.

data = b'abcdef' print(data[-3:]) # b'def' print(data[::-1]) # b'fedcba' print(data[::2]) # b'ace'

A negative step reverses the traversal order. When step is negative, the default start becomes the end and the default stop becomes the beginning, so data[::-1] reverses the entire sequence. Be careful with explicit boundaries in reversed slices: data[4:1:-1] yields b'edc' because start is 4, stop is 1 (exclusive), and the step moves backward.

Slicing Always Returns a New Bytes Object

Unlike a memoryview, slicing a bytes object always performs a copy. The result is a new bytes object with its own memory. This has two practical consequences:

  • The original bytes object remains unchanged.
  • The slice can be safely stored or passed around without keeping the original buffer alive.
original = b'hello world' slice_obj = original[6:] print(slice_obj) # b'world' print(original) # b'hello world' print(slice_obj is original) # False

If you need a view that does not copy data, use a memoryview instead. Slicing a memoryview returns a new memoryview object that references the same underlying buffer. This is crucial when working with large binary payloads where copying would waste memory and CPU.

data = b'x' * 1000000 view = memoryview(data) segment = view[100:200] # no copy print(segment.nbytes) # 100

Slicing bytearray and memoryview

bytearray is the mutable counterpart of bytes. Slicing a bytearray also returns a new bytearray (a copy), not a view. However, you can assign to a slice of a bytearray to modify it in place, which is not possible with bytes.

buf = bytearray(b'abcdef') buf[1:3] = b'XY' print(buf) # bytearray(b'aXYdef')

For memoryview, slicing returns a new memoryview that shares the original buffer. This allows zero-copy access to a sub-region. You can also cast the view to a different format, but the slice operation itself remains non-copying.

data = b'abcdef' view = memoryview(data) sub = view[2:5] print(sub.tobytes()) # b'cde'

Keep in mind that a memoryview keeps the original object alive as long as the view exists. If you only need a small slice from a large bytes object, a memoryview prevents the large object from being garbage collected until the view is released.

Common Mistakes and Edge Cases

Several edge cases trip up developers new to bytes slicing.

Step zero raises a ValueError. The step cannot be zero because that would create an infinite loop.

data = b'abc' # data[::0] # ValueError: slice step cannot be zero

Out-of-range indices do not raise an error. Python clamps the slice boundaries to the length of the sequence. For example, data[10:20] on a 5-byte object returns b''.

data = b'abc' print(data[5:10]) # b''

Slicing with a string as an index raises a TypeError. Bytes indices must be integers or slices, not strings.

data = b'abc' # data['1'] # TypeError: 'str' object cannot be interpreted as an integer

Empty slices are common when parsing variable-length fields. A slice that ends before it starts (with a positive step) returns an empty bytes object, which is falsy. This can be used for control flow.

header = b'\x00\x01' if header[1:1]: pass # not executed

Performance Considerations for Large Byte Sequences

Because slicing a bytes object copies the data, repeated slicing of a large buffer can lead to quadratic time and memory overhead. For example, extracting many small fields from a 100 MB file by slicing repeatedly creates many new bytes objects, each holding only a few bytes but requiring a copy.

If you only need to read values, a memoryview avoids the copy. Use memoryview when you need to parse a large binary structure and want to avoid duplicating the underlying data.

def parse_header(data): view = memoryview(data) version = view[0] length = int.from_bytes(view[1:5], 'big') payload = view[5:5+length] return version, length, payload.tobytes()

This pattern keeps the original buffer referenced only for the duration of the parse, and the final tobytes() call creates a copy only for the payload you actually need.

Another performance aspect is that slicing a bytes object is O(k) where k is the slice length. For very small slices, the overhead is negligible, but for many slices, the cumulative cost adds up. If you are processing a stream, consider using io.BytesIO or a memoryview to avoid repeated copies.

Practical Examples: Extracting Binary Fields

Bytes slicing is essential when parsing binary protocols. Consider a simple network packet with a 1-byte type, a 2-byte length, and a variable payload.

def parse_packet(packet): if len(packet) < 3: raise ValueError('packet too short') ptype = packet[0] length = int.from_bytes(packet[1:3], 'big') if len(packet) < 3 + length: raise ValueError('payload length mismatch') payload = packet[3:3+length] return ptype, length, payload

Here packet[1:3] extracts the length field, and packet[3:3+length] extracts the payload. The slice boundaries are computed from the parsed length, which is a common pattern.

For fixed-width records, slicing with a step can be used to extract every Nth byte, such as reading color channels from an interleaved RGB buffer.

rgb_data = b'\x00\x10\x20\x01\x11\x21' reds = rgb_data[0::3] # b'\x00\x01' greens = rgb_data[1::3] # b'\x10\x11' blues = rgb_data[2::3] # b'\x20\x21'

When the data is not a multiple of the step, the last slice simply ends at the last available index. This is a clean way to demultiplex interleaved data without a loop.

Slicing vs. Other Extraction Methods

Sometimes slicing is not the most appropriate tool. For example, bytes.startswith() or bytes.find() are better for searching, and int.from_bytes() is the standard way to convert a slice to an integer. Slicing is the right choice when you need a contiguous sub-sequence as a bytes object. If you need to iterate over individual bytes, a slice is unnecessary; you can index directly.

For mutable data, bytearray slice assignment is a powerful tool for in-place editing. This is useful when building a buffer incrementally.

buf = bytearray(b'abcdef') buf[2:4] = b'XYZ' # replaces 'cd' with 'XYZ' print(buf) # bytearray(b'abXYZef')

This operation can change the length of the bytearray, which is a key difference from list slice assignment where the replacement must be iterable. For bytes, the replacement must be a bytes-like object.

Understanding when to use slicing versus memoryview or bytearray depends on whether you need a copy, a view, or mutability. For read-only parsing, a memoryview avoids copies. For small extractions, a direct slice is simpler and clearer. For in-place modifications, bytearray slice assignment is the only option.

Mastering python bytes slicing means knowing not only the [start:stop:step] syntax but also the copy semantics and the alternatives available. With this knowledge, you can handle binary data efficiently and avoid the common pitfalls that lead to memory bloat or subtle off-by-one errors.

python bytes slicing: Practical Usage and Code Examples | RYUSLOG DEV