Slicing in Python: Beyond the Basics of start:stop:step
- seq[start:stop] returns items from start up to, but not including, stop.
- Negative indices count from the end; omitting start or stop defaults to the sequence's beginning or end.
- A step value of -1 reverses a sequence: seq[::-1].
- Assigning to a slice can replace, grow, or shrink a list in place, without needing an explicit loop.
Basic slice syntax
Slicing extracts a subsequence from any sequence type — lists, tuples, strings — using the form seq[start:stop]. It returns items starting at index start, up to but not including index stop:
letters = ["a", "b", "c", "d", "e"]
print(letters[1:3]) # ['b', 'c']
print(letters[0:2]) # ['a', 'b']
word = "Python"
print(word[0:3]) # 'Pyt'
The result is always a new object of the same type as the original — slicing a list returns a list, slicing a string returns a string — and the original sequence is left untouched.
Negative indices and open-ended slices
Negative indices count backward from the end of the sequence, with -1 referring to the last element. Omitting start defaults to the beginning; omitting stop defaults to the end:
letters = ["a", "b", "c", "d", "e"]
print(letters[-2:]) # ['d', 'e'] last two items
print(letters[:-2]) # ['a', 'b', 'c'] everything except the last two
print(letters[:]) # a shallow copy of the whole list
seq[:] is a common idiom for making a shallow copy of a list — the new list is a distinct object, but if its elements are themselves mutable, they are shared with the original.
Step values and reversing
A third value, seq[start:stop:step], controls how many items are skipped between each included element. A negative step walks backward:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::2]) # [0, 2, 4, 6, 8] every other item
print(numbers[1::2]) # [1, 3, 5, 7, 9] every other, starting at index 1
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] reversed
print(numbers[8:2:-2]) # [8, 6, 4]
With a negative step, start and stop still describe the same index positions, but the traversal direction runs from higher to lower indices — this is why numbers[8:2:-2] begins at index 8 and stops before index 2.
Assigning to a slice
Slices are not just for reading — assigning to a slice of a list replaces that section in place, and the replacement does not need to be the same length:
letters = ["a", "b", "c", "d", "e"]
letters[1:3] = ["X", "Y", "Z"]
print(letters) # ['a', 'X', 'Y', 'Z', 'd', 'e']
letters[1:4] = [] # deletes those three items
print(letters) # ['a', 'd', 'e']
Assigning an empty list to a slice removes those elements entirely, which is a clean alternative to looping with del or repeated .pop() calls when removing a contiguous block. Strings and tuples are immutable, so slice assignment only works on mutable sequences like lists.
The slice() object
The colon syntax is shorthand for a built-in slice object, which you can construct directly and reuse across multiple sequences — useful when the same slicing logic applies in several places:
middle = slice(1, -1)
print([10, 20, 30, 40][middle]) # [20, 30]
print("Python"[middle]) # 'ytho'
Storing a slice object in a variable also makes intent clearer in code that slices the same range repeatedly, such as stripping a fixed-width header from many rows of tabular data.
Why out-of-range slices never raise
Indexing a sequence with a single out-of-range index raises IndexError, but slicing with out-of-range bounds never does — it simply clamps to the available items:
letters = ["a", "b", "c"]
print(letters[10]) # IndexError: list index out of range
print(letters[1:100]) # ['b', 'c'] -- clamped, no error
print(letters[100:200]) # [] -- empty, still no error
This forgiving behaviour makes slicing convenient for defensive code: text[:50] safely truncates a string to at most fifty characters even if the string is shorter, with no need for a length check beforehand. The same clamping applies to the step value's start and stop bounds, which is why reversing a list with seq[::-1] works regardless of the list's length, including an empty list.
Slicing works on tuples the same way
Everything covered so far for lists and strings applies identically to tuples — the same start:stop:step syntax, the same clamping on out-of-range bounds, the same negative-index counting from the end:
coordinates = (10, 20, 30, 40, 50)
print(coordinates[1:3]) # (20, 30)
print(coordinates[::-1]) # (50, 40, 30, 20, 10)
The one difference is that a tuple, like a string, is immutable, so tuple slice assignment is never possible — coordinates[1:3] = (99, 98) raises TypeError. If you need to change part of a tuple, the usual approach is to build a brand-new tuple by concatenating the unchanged slices with the replacement values: coordinates[:1] + (99, 98) + coordinates[3:].