Skip to content
datarekha
Python Easy Asked at AmazonAsked at Google

How does Python list slicing work, including step and negative indices?

The short answer

Slicing uses the syntax `seq[start:stop:step]` and returns a new list containing elements from index `start` up to but not including `stop`, stepping by `step`. Negative indices count from the end; a negative step reverses direction. Omitted parts default to the beginning, end, or step of 1.

How to think about it

Slicing looks trivial, but the edge cases — negative indices, negative steps, out-of-range bounds — trip up a lot of candidates. The cleanest way to explain it is to build up the three parameters in order, and to carry one mental model: the indices sit between elements, not on them.

The full form is seq[start:stop:step], and stop is exclusive — the element at stop is left out, matching range(start, stop). A negative index -k means len(seq) - k, so -1 is the last element. A negative step walks backwards.

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

data[2:5]      # [2, 3, 4]          — stop=5 excluded
data[:4]       # [0, 1, 2, 3]       — omitted start = beginning
data[6:]       # [6, 7, 8, 9]       — omitted stop = end
data[:]        # a full shallow copy
data[::2]      # [0, 2, 4, 6, 8]    — every other
data[::-1]     # [9, 8, ..., 0]     — reversed
data[-3:]      # [7, 8, 9]          — last three
data[8:2:-1]   # [8, 7, 6, 5, 4, 3] — step backwards

A worked example

data = list(range(10))
print("data:", data)

print("[2:5]  ->", data[2:5])      # indices 2,3,4
print("[::2]  ->", data[::2])      # every other
print("[1::2] ->", data[1::2])     # odd indices
print("[::-1] ->", data[::-1])     # reversed
print("[-3:]  ->", data[-3:])      # last three
print("[-3:-1]->", data[-3:-1])    # stop still exclusive

# Out-of-range slices clamp silently — no IndexError
print("[100:] ->", data[100:])     # []
print("[:100] ->", data[:100])     # whole list

# Slice assignment edits in place — and can change the length
nums = [1, 2, 3, 4, 5]
nums[1:3] = [20, 30, 40]           # replace two elements with three
print("After slice assign:", nums)

# Strings and tuples slice by the same rules
print("string slice:", "hello world"[6:])
print("tuple slice :", (1, 2, 3, 4)[1:3])
data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[2:5]  -> [2, 3, 4]
[::2]  -> [0, 2, 4, 6, 8]
[1::2] -> [1, 3, 5, 7, 9]
[::-1] -> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[-3:]  -> [7, 8, 9]
[-3:-1]-> [7, 8]
[100:] -> []
[:100] -> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
After slice assign: [1, 20, 30, 40, 4, 5]
string slice: world
tuple slice : (2, 3)

Two behaviours worth naming. First, slices clamp instead of raising: data[100:] returns [] where the single-index data[100] would throw IndexError — a slice describes a range, so an empty overlap is a legal answer, not an error. Second, slice assignment can resize the list: nums[1:3] = [20, 30, 40] swapped two elements for three, growing the list in place.

The same rules everywhere

Strings and tuples slice identically, each returning a new object of its own type. String slicing is a tidy way to pull fixed-width fields — row[10:20] — without reaching for a regex.

Learn it properly Lists

Keep practising

All Python questions

Explore further