Home Tutorials Core Syntax

Core Syntax

Slicing in Python: Beyond the Basics of start:stop:step

Pyford Notes July 6, 2026 6 min read
Key points
  • 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:].