Python - List Slicing with Examples

Python's slice notation follows the pattern `[start:stop:step]`. The `start` index is inclusive, `stop` is exclusive, and `step` determines the increment between elements. All three parameters are...

Key Insights

  • List slicing uses the syntax list[start:stop:step] where start is inclusive, stop is exclusive, and step determines the increment—all parameters are optional
  • Negative indices count from the end of the list, enabling powerful reverse operations and tail extraction without calculating list length
  • Slice assignments can modify lists in-place, insert elements, or delete ranges, making slicing a versatile tool for list manipulation beyond simple extraction

Understanding Slice Syntax

Python’s slice notation follows the pattern [start:stop:step]. The start index is inclusive, stop is exclusive, and step determines the increment between elements. All three parameters are optional, with defaults of 0, len(list), and 1 respectively.

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Basic slicing - start to stop
print(numbers[2:5])  # [2, 3, 4]

# Omit start - begins at index 0
print(numbers[:4])   # [0, 1, 2, 3]

# Omit stop - goes to end
print(numbers[6:])   # [6, 7, 8, 9]

# Omit both - creates a shallow copy
print(numbers[:])    # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

The step parameter controls the interval between selected elements:

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Every second element
print(numbers[::2])     # [0, 2, 4, 6, 8]

# Every third element starting at index 1
print(numbers[1::3])    # [1, 4, 7]

# Every second element from index 2 to 8
print(numbers[2:8:2])   # [2, 4, 6]

Negative Indices and Reverse Slicing

Negative indices count backward from the end of the list, where -1 refers to the last element, -2 to the second-to-last, and so on. This eliminates manual length calculations.

data = ['a', 'b', 'c', 'd', 'e', 'f']

# Last three elements
print(data[-3:])      # ['d', 'e', 'f']

# All but last two
print(data[:-2])      # ['a', 'b', 'c', 'd']

# From third element to second-to-last
print(data[2:-1])     # ['c', 'd', 'e']

# Last element only
print(data[-1:])      # ['f']

Negative step values reverse the direction of iteration:

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Reverse entire list
print(numbers[::-1])    # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# Every second element in reverse
print(numbers[::-2])    # [9, 7, 5, 3, 1]

# Reverse slice from index 7 to 2
print(numbers[7:2:-1])  # [7, 6, 5, 4, 3]

# Reverse from index 8 to beginning
print(numbers[8::-1])   # [8, 7, 6, 5, 4, 3, 2, 1, 0]

Slice Assignment and Modification

Slices can appear on the left side of assignments to modify lists in-place. This enables insertion, replacement, and deletion operations without explicit method calls.

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Replace a range
numbers[2:5] = [20, 30, 40]
print(numbers)  # [0, 1, 20, 30, 40, 5, 6, 7, 8, 9]

# Replace with different-sized sequence
numbers[2:5] = [100]
print(numbers)  # [0, 1, 100, 5, 6, 7, 8, 9]

# Insert elements without replacing
numbers[2:2] = [50, 60]
print(numbers)  # [0, 1, 50, 60, 100, 5, 6, 7, 8, 9]

# Delete a range
numbers[2:5] = []
print(numbers)  # [0, 1, 5, 6, 7, 8, 9]

Step-based slice assignment requires the replacement sequence to match the slice length:

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Replace every second element
numbers[::2] = [10, 20, 30, 40, 50]
print(numbers)  # [10, 1, 20, 3, 30, 5, 40, 7, 50, 9]

# This raises ValueError - length mismatch
# numbers[::2] = [1, 2, 3]

Practical Applications

Extracting Substrings from Structured Data

log_entries = [
    "2024-01-15 ERROR Database connection failed",
    "2024-01-15 INFO User logged in",
    "2024-01-15 WARN High memory usage",
    "2024-01-16 ERROR API timeout"
]

# Extract dates (first 10 characters)
dates = [entry[:10] for entry in log_entries]
print(dates)  # ['2024-01-15', '2024-01-15', '2024-01-15', '2024-01-16']

# Extract log levels (characters 11-16)
levels = [entry[11:16] for entry in log_entries]
print(levels)  # ['ERROR', 'INFO ', 'WARN ', 'ERROR']

# Extract messages (everything after position 17)
messages = [entry[17:] for entry in log_entries]
print(messages)  
# ['Database connection failed', 'User logged in', 'High memory usage', 'API timeout']

Windowing and Batching

def sliding_window(data, window_size):
    """Generate sliding windows over a list."""
    return [data[i:i+window_size] for i in range(len(data) - window_size + 1)]

time_series = [10, 15, 13, 18, 20, 22, 19, 25]
windows = sliding_window(time_series, 3)
print(windows)
# [[10, 15, 13], [15, 13, 18], [13, 18, 20], [18, 20, 22], [20, 22, 19], [22, 19, 25]]

# Calculate moving averages
moving_avg = [sum(window) / len(window) for window in windows]
print(moving_avg)
# [12.666..., 15.333..., 17.0, 20.0, 20.333..., 22.0]

Chunking Data for Processing

def chunk_list(data, chunk_size):
    """Split list into fixed-size chunks."""
    return [data[i:i+chunk_size] for i in range(0, len(data), chunk_size)]

items = list(range(23))
batches = chunk_list(items, 5)
print(batches)
# [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], 
#  [15, 16, 17, 18, 19], [20, 21, 22]]

# Process batches
for batch_num, batch in enumerate(batches, 1):
    print(f"Processing batch {batch_num}: {len(batch)} items")

Removing Elements by Pattern

# Remove every third element
data = list(range(20))
del data[::3]
print(data)  # [1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19]

# Keep only even indices
numbers = [10, 20, 30, 40, 50, 60, 70, 80]
numbers[1::2] = []
print(numbers)  # [10, 30, 50, 70]

Performance Considerations

Slicing creates new list objects, which impacts memory for large datasets. Understanding when copies are made versus when references are used is critical.

import sys

original = list(range(1000000))
slice_copy = original[::2]

print(f"Original size: {sys.getsizeof(original)} bytes")
print(f"Slice size: {sys.getsizeof(slice_copy)} bytes")

# Slices are shallow copies
matrix = [[1, 2], [3, 4], [5, 6]]
subset = matrix[1:]
subset[0][0] = 999
print(matrix)  # [[1, 2], [999, 4], [5, 6]] - original modified!

For memory-efficient iteration over large sequences, use itertools.islice instead:

from itertools import islice

large_data = range(10000000)

# Memory-efficient - doesn't create intermediate list
for chunk in islice(large_data, 1000, 2000):
    pass  # Process chunk

# Compare with list slicing (creates list in memory)
# chunk = list(large_data)[1000:2000]  # Inefficient

Edge Cases and Common Pitfalls

Slicing handles out-of-bounds indices gracefully, unlike direct indexing:

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

# Direct access raises IndexError
# print(data[10])

# Slicing returns empty list or truncated result
print(data[10:])     # []
print(data[3:100])   # [4, 5]
print(data[-100:2])  # [1, 2]

Empty slices and reversed ranges:

numbers = [0, 1, 2, 3, 4, 5]

# Start >= stop with positive step returns empty
print(numbers[4:2])    # []

# Requires negative step for reverse range
print(numbers[4:2:-1]) # [4, 3]

# Empty slice assignment deletes range
numbers[2:4] = []
print(numbers)  # [0, 1, 4, 5]

Understanding these mechanics enables precise list manipulation without verbose loops or temporary variables. Slicing remains one of Python’s most elegant features for sequence operations, combining readability with efficiency when applied appropriately.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.