Iterating Different Portion of a List with Different Step Size in Python: Complete List Slicing and Iteration Guide

Iterating different portion of a list with different step size in python

Iterating different portion of a list with different step size in python

I remember the moment slicing really clicked for me — I was trying to process every third element of a large dataset, and instead of writing a clunky loop with an index counter and a modulo check, someone showed me my_list[::3]. It felt like discovering a shortcut I’d been missing the whole time. Since then, step-based slicing has become one of my favorite Python features, and I want to share everything I’ve learned about using it to iterate over different portions of a list.

The Basics of List Slicing

Python’s slicing syntax follows this pattern:

list_name[start:stop:step]
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(numbers[2:8])      # start and stop only
print(numbers[2:8:2])    # start, stop, and step
print(numbers[::2])      # every second element
print(numbers[::3])      # every third element

Output:

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

Iterating With a Step Size Directly

To actually loop over these slices, I typically just combine slicing with a for loop:

numbers = [10, 20, 30, 40, 50, 60, 70, 80]

for value in numbers[::2]:
    print(value, end=" ")

Output:

10 30 50 70

This gives me every second element starting from index 0. I can adjust the starting index too:

for value in numbers[1::2]:
    print(value, end=" ")

Output:

20 40 60 80

Negative Step: Iterating in Reverse

One of the things I love about slicing is how naturally it handles reverse iteration:

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

print(numbers[::-1])       # full reverse
print(numbers[::-2])       # reverse, every second element
print(numbers[4:1:-1])     # reverse with explicit bounds

Output:

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

With a negative step, start needs to be greater than stop for the slice to return anything, since Python is now moving backward through the indices.

Iterating Different Portions With Different Steps

This is where things get genuinely useful for real tasks — combining multiple slices with different step sizes in the same piece of logic. Say I have a dataset where the first half needs fine-grained processing and the second half can be sampled more sparsely:

data = list(range(20))

first_half = data[:10:1]     # every element in first half
second_half = data[10::4]    # every 4th element in second half

print("First half:", first_half)
print("Second half:", second_half)

for value in first_half + second_half:
    print(value, end=" ")

Output:

First half: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Second half: [10, 14, 18]
0 1 2 3 4 5 6 7 8 9 10 14 18

I use this pattern often when I need denser sampling in one region of data and sparser sampling in another — for example, when downsampling time-series data that has more meaningful detail early on.

Using enumerate() Alongside Slicing

Sometimes I need the original index while iterating over a slice, so I combine enumerate() with slicing:

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

for i, value in enumerate(data[::2]):
    print(f"Slice index {i}, value: {value}")

Output:

Slice index 0, value: a
Slice index 2, value: c
Slice index 4, value: e
Slice index 6, value: g

Note that enumerate() here counts positions within the slice, not the original list, which is a subtle detail that’s tripped me up before. If I need the original index, I calculate it manually:

data = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
step = 2

for original_index in range(0, len(data), step):
    print(f"Original index {original_index}, value: {data[original_index]}")

Output:

Original index 0, value: a
Original index 2, value: c
Original index 4, value: e
Original index 6, value: g

Internal Working: How Slicing Actually Computes Indices

Under the hood, when Python evaluates list[start:stop:step], it doesn’t loop through the list checking each index one by one in Python-level code. Instead, it calls the C-level slice.indices(length) logic, which normalizes start, stop, and step (handling negative values and out-of-range values) and then directly computes which indices to include. This is implemented efficiently in CPython, and the resulting slice is built by copying the relevant elements into a new list object.

Because slicing creates a brand-new list, it does carry a memory cost proportional to the slice size — something to keep in mind for very large lists where you might prefer itertools.islice() instead, which produces an iterator rather than a fully materialized list.

Using itertools.islice() for Memory Efficiency

When I’m working with very large sequences or generators where I don’t want to build an entire sliced list in memory, I reach for itertools.islice():

from itertools import islice

def large_generator():
    for i in range(1_000_000):
        yield i

# Get every 100,000th element without materializing a huge list
for value in islice(large_generator(), 0, 1_000_000, 100_000):
    print(value, end=" ")

Output:

0 100000 200000 300000 400000 500000 600000 700000 800000 900000

Unlike list slicing, islice() works lazily and is compatible with any iterable, not just sequences that support indexing.

Real-World Applications

Common Mistakes

  1. Forgetting that negative steps require start > stopnumbers[2:8:-1] returns an empty list because the direction doesn’t match the bounds.
  2. Assuming slicing mutates the original list — It doesn’t. Slicing always returns a new list, leaving the original untouched.
  3. Using enumerate() and expecting original indices — As shown above, enumerate() on a slice restarts counting from zero relative to the slice, not the source list.
  4. Off-by-one errors with stop values — Remember that stop is exclusive, so numbers[0:5] gives five elements (indices 0–4), not six.
  5. Using list slicing on very large data when memory matters — Prefer islice() from itertools for huge or infinite iterables.

Debugging Tips

When step-based logic isn’t producing the output I expect, I like to manually check what slice.indices() resolves to:

s = slice(1, 20, 3)
print(s.indices(20))

Output:

(1, 20, 3)

This confirms exactly which start, stop, and step values Python will actually use once negative or out-of-range values are normalized, which is especially helpful for debugging negative step edge cases.

Using Slice Objects for Reusable Step Logic

When I find myself using the same start/stop/step combination in multiple places, I define a reusable slice object instead of repeating the literal slice syntax everywhere:

data = list(range(30))

every_third = slice(0, None, 3)
every_fifth = slice(0, None, 5)

print(data[every_third])
print(data[every_fifth])

Output:

[0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
[0, 5, 10, 15, 20, 25]

This has been especially handy when I need to apply the exact same slicing pattern consistently across several different lists or arrays in the same script, since it keeps the step logic defined in one place rather than scattered as magic numbers.

Combining Slicing With List Comprehensions for Filtered Sampling

Sometimes I want to sample with a step size and apply a filter or transformation at the same time. Combining slicing with a list comprehension covers this cleanly:

data = list(range(1, 51))

# Every 5th value, but only keep it if it's even
result = [value for value in data[::5] if value % 2 == 0]
print(result)

Output:

[10, 20, 30, 40, 50]

I reach for this pattern often when preparing quick data previews — taking a manageable, evenly spaced sample of a large dataset and simultaneously filtering out values I don’t care about for a given check.

FAQs

Can I use a step size on strings and tuples too? Yes, slicing with a step works on any sequence type, including strings, tuples, and range objects.

What happens if I slice with step 0? Python raises a ValueError: slice step cannot be zero.

Does slicing work on dictionaries? No, dictionaries aren’t sequences and don’t support slicing directly. You’d need to work with list(dict.items()) first.

Is slicing faster than a manual loop with modulo checks? Generally yes, because slicing is implemented in optimized C code, while a manual loop with an if i % step == 0 check runs at the slower Python bytecode level.

Summary

Step-based list slicing is one of the most elegant tools Python offers for iterating over different portions of data at different densities. Whether I’m sampling every Nth element, reversing a sequence, or combining multiple slices with different steps for different sections of data, this feature consistently replaces what would otherwise be verbose manual loops. For very large datasets, pairing this knowledge with itertools.islice() keeps memory usage under control.

References

Exit mobile version