Advanced Slicing and Pattern Unpacking

Updated

September 7, 2026

Advanced Slicing and Pattern Unpacking

After reading this chapter, you will master Python’s slicing mechanics ([start:stop:step]), employ reusable slice objects for fixed-width parsing, manipulate negative strides for sequence reversal, execute in-place slice mutations, and destructure variable-length collections with extended starred unpacking.

Mental model

Slicing extracts a subset of a sequence using half-open intervals: [start:stop) where start is inclusive and stop is exclusive.

Index Mapping:
   Indices:   0   1   2   3   4   5
   Items:   [ P   Y   T   H   O   N ]
  -Indices:  -6  -5  -4  -3  -2  -1

Slicing Interval: [1:4] -> Extracts indices 1, 2, 3 ('Y', 'T', 'H')

The Stride Equation

When specifying a step (s[start:stop:step]), indices are computed as:

\[\text{index}_k = \text{start} + k \times \text{step} \quad \text{for } k = 0, 1, 2, \dots\]

When step < 0, indexing moves backward from start down to stop (exclusive).

Extended Starred Unpacking

Python allows unpacking arbitrary iterables into target variables, capturing any remaining elements into a list with a starred name (*rest):

Tuple: (10, "auth", 200, 0.45, 128, "OK")
Unpack: (code, svc, *metrics, status)
          │     │        │       │
          10  "auth"  [200, 0.45, 128]  "OK"

Minimal example

Save as slicing_and_unpacking.py:

# slicing_and_unpacking.py
def main() -> None:
    # 1. Extended starred unpacking
    telemetry_packet = ("node-01", "2026-09-07T10:00:00Z", 42.1, 88.4, 12.0, 99.1, "STABLE")
    node, timestamp, *metrics, status = telemetry_packet

    print(f"Node ID   : {node}")
    print(f"Timestamp : {timestamp}")
    print(f"Metrics   : {metrics} (Captured {len(metrics)} sensor values)")
    print(f"Status    : {status}")

    # 2. Named Slice Objects for Fixed-Width Record Parsing
    raw_log = "2026-09-07 10:15:00 [ERROR] Connection timeout to database host"
    TIMESTAMP = slice(0, 19)
    LEVEL     = slice(20, 27)
    MESSAGE   = slice(28, None)  # None runs to the end of string

    print(f"\nParsed log using slice objects:")
    print(f"  Time    : {raw_log[TIMESTAMP]}")
    print(f"  Level   : {raw_log[LEVEL]}")
    print(f"  Message : {raw_log[MESSAGE]}")

if __name__ == "__main__":
    main()

Run via uv run python slicing_and_unpacking.py:

Node ID   : node-01
Timestamp : 2026-09-07T10:00:00Z
Metrics   : [42.1, 88.4, 12.0, 99.1] (Captured 4 sensor values)
Status    : STABLE

Parsed log using slice objects:
  Time    : 2026-09-07 10:15:00
  Level   : [ERROR]
  Message : Connection timeout to database host

Worked examples

Case 1: In-Place Slice Replacement and Resizing

Unlike immutable sequences (strings and tuples), a list supports in-place assignment to slices. You can replace, shrink, or expand a slice dynamically:

# slice_mutation.py
def mutate_buffer() -> None:
    buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    print(f"Initial buffer: {buffer}")

    # Replace elements at indices 2, 3, 4 with a smaller list (shrinks the buffer)
    buffer[2:5] = [99]
    print(f"After buffer[2:5] = [99]: {buffer}")

    # Replace a single index with multiple elements (expands the buffer)
    buffer[5:6] = [700, 800, 900]
    print(f"After buffer[5:6] = [700, 800, 900]: {buffer}")

    # Clear elements at even indices using extended stride
    del buffer[::2]
    print(f"After del buffer[::2]: {buffer}")

if __name__ == "__main__":
    mutate_buffer()

Run:

uv run python slice_mutation.py

Output:

Initial buffer: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
After buffer[2:5] = [99]: [0, 1, 99, 5, 6, 7, 8, 9]
After buffer[5:6] = [700, 800, 900]: [0, 1, 99, 5, 6, 700, 800, 900, 8, 9]
After del buffer[::2]: [1, 5, 700, 900, 9]

Case 2: Parsing Protocol Frames with Named Slices

When parsing binary or fixed-width protocol headers, hardcoding numerical slice boundaries throughout a codebase creates brittle logic. Storing slice instances in configuration objects keeps parsers readable and maintainable:

# frame_parser.py
class FrameFormat:
    MAGIC_BYTES = slice(0, 4)
    VERSION     = slice(4, 6)
    PACKET_ID   = slice(6, 10)
    PAYLOAD     = slice(10, None)

def parse_network_frame(raw_hex: str) -> dict[str, str]:
    return {
        "magic": raw_hex[FrameFormat.MAGIC_BYTES],
        "version": raw_hex[FrameFormat.VERSION],
        "packet_id": raw_hex[FrameFormat.PACKET_ID],
        "payload": raw_hex[FrameFormat.PAYLOAD],
    }

if __name__ == "__main__":
    incoming_frame = "4155544801020000002a48656c6c6f20576f726c64"
    fields = parse_network_frame(incoming_frame)
    for k, v in fields.items():
        print(f"Field {k:10} : {v}")

Run:

uv run python frame_parser.py

Output:

Field magic      : 41555448
Field version    : 0102
Field packet_id  : 0000002a
Field payload    : 48656c6c6f20576f726c64

Case 3: Starred Head-Tail Splitting for Recursive Processing

Extended unpacking provides clean head-tail deconstruction without index arithmetic:

# pipeline_reducer.py
from collections.abc import Callable

def apply_middleware_pipeline(
    payload: str, 
    middlewares: list[Callable[[str], str]]
) -> str:
    if not middlewares:
        return payload
    
    # Deconstruct head and remaining tail
    current_handler, *remaining_handlers = middlewares
    transformed = current_handler(payload)
    return apply_middleware_pipeline(transformed, remaining_handlers)

if __name__ == "__main__":
    pipeline: list[Callable[[str], str]] = [
        lambda s: s.strip(),
        lambda s: s.lower(),
        lambda s: s.replace(" ", "_"),
        lambda s: f"validated://{s}",
    ]

    raw_input = "   System Production Cluster Alpha   "
    processed = apply_middleware_pipeline(raw_input, pipeline)
    print(f"Raw input : '{raw_input}'")
    print(f"Processed : '{processed}'")

Run:

uv run python pipeline_reducer.py

Output:

Raw input : '   System Production Cluster Alpha   '
Processed : 'validated://system_production_cluster_alpha'

Pitfalls

Pitfall 1: Assigning a Non-Iterable to a Slice

Slice assignment replaces a range of items, so the right-hand side must be an iterable:

lst = [1, 2, 3, 4]

# THE BUG:
lst[1:3] = 99  # TypeError: can only assign an iterable

# THE FIX: Wrap the replacement in an iterable (such as a list)
lst[1:3] = [99]  # Result: [1, 99, 4]

Pitfall 2: Multiple Starred Expressions in a Single Target

You can only have one starred expression in an unpacking assignment. Python cannot resolve ambiguity if multiple wildcards exist:

data = [1, 2, 3, 4, 5]

# THE BUG:
*first, *second = data  # SyntaxError: multiple starred expressions in assignment

# VALID:
first, *middle, last = data

Exercises

  1. Given the string text = "abcdefghijklmnopqrstuvwxyz", write a single slicing expression that extracts every third letter in reverse order.
  2. Given a list data = [10, 20, 30, 40, 50], write a slice assignment statement that replaces the middle three elements [20, 30, 40] with the single element [999].
  3. Given a variable-length list containing at least two elements, use extended unpacking to assign the first element to head, the last element to tail, and the remaining items to body.
  4. Demonstrate why s[:] creates a shallow copy of a list, and show what happens when the inner elements are mutable lists.

Further reading

  • Python Documentation: The slice object and sequence indexing.
  • PEP 3132: Extended Iterable Unpacking.
  • Python Reference Manual: Assignment statements.