How slicing works in Python

Slicing is one of the things that makes Python pleasant, and one of the things people half-learn and then guess at. This article covers the grammar, the two behaviours that surprise people, and what a slice copy actually copies.

The grammar

a[start:stop:step], with every part optional:

a          : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
a[2:5]     : [2, 3, 4]      <- stop is exclusive
a[:3]      : [0, 1, 2]
a[7:]      : [7, 8, 9]
a[::2]     : [0, 2, 4, 6, 8]
a[1:8:3]   : [1, 4, 7]
a[:]       : [0, 1, ...]    <- a copy

stop being exclusive is what makes a[:n] and a[n:] fit together with no overlap and no gap, and it is why len(a[x:y]) is simply y - x when both are in range.

Negative indices count from the end:

a[-1]      : 9
a[-3:]     : [7, 8, 9]
a[:-3]     : [0, 1, 2, 3, 4, 5, 6]

Reversing, and the direction rule

a[::-1]    : [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
a[::-2]    : [9, 7, 5, 3, 1]

With a negative step the traversal runs backwards, so start must be after stop:

a[5:2:-1]  : [5, 4, 3]   <- start > stop when step is negative
a[2:5:-1]  : []          <- empty, the bounds point the wrong way

The second one is the mistake to watch for. It does not raise; it hands back an empty list, and your code carries on with nothing.

Slices never raise IndexError

a[20:30]   : []
a[5:100]   : [5, 6, 7, 8, 9]
a[20]      -> IndexError: list index out of range

Indexing is strict, slicing clamps. This is genuinely useful — it is why page[offset:offset + size] needs no bounds check, and why the chunking loop in the article on splitting lists needs no special case for the final short chunk.

It also means a typo in a slice produces an empty list rather than an error, which cuts the other way.

A slice copy is shallow

a[:] is a common idiom for copying a list. It copies one level:

copy is nested          : False
copy[0] is nested[0]    : True   <- inner lists shared
after copy[0].append(99), nested = [[1, 2, 99], [3, 4]]

The outer list is new. The inner lists are the same objects. Appending through what you thought was a copy changed the original.

list.copy() behaves identically. Only copy.deepcopy gives you an independent structure:

orig[:]          inner shared : True
orig.copy()      inner shared : True
copy.deepcopy    inner shared : False

Assigning to a slice

You can assign to a slice, and for a plain slice the lengths need not match — the list resizes:

b[1:3] = ['x','y','z'] -> [0, 'x', 'y', 'z', 3, 4]
b[1:4] = []            -> [0, 3, 4]

So b[1:4] = [] is another way of writing del b[1:4].

An extended slice — one with a step — is different. There the lengths must match exactly:

c[::2] = [0, 0] -> ValueError: attempt to assign sequence of size 2 to extended slice of size 3
c[::2] = [9,9,9] -> [9, 1, 9, 3, 9]

Which makes sense: there is no sensible way to insert into every second position.

The slice object

The syntax is sugar for a slice object, and you can build one yourself:

slice(2, 5)          : slice(2, 5, None)
a[s]                 : [2, 3, 4]
s.indices(len(a))    : (2, 5, 1)

Which is worth knowing because it lets you name one:

LAST_THREE = slice(-3, None)
a[LAST_THREE] -> [7, 8, 9]

For fixed-width record parsing, a handful of named slices beats a scattering of magic numbers.

Your own classes receive that object in __getitem__:

p[3]     : got 3 of type int
p[1:2]   : got slice(1, 2, None) of type slice
p[1:2:3] : got slice(1, 2, 3) of type slice

It works on any sequence

'hello world'[::2]   : 'hlowrd'
(1,2,3,4)[1:3]       : (2, 3)
b'abcd'[1:3]         : b'bc'
range(10)[2:5]       : range(2, 5)   <- still a range, lazily

The range case is a nice one: slicing a range gives another range rather than materialising anything.

Mappings are not sequences, so a dict has nothing to slice — and the failure is not the one you would guess:

a dict -> KeyError: slice('a', 'b', None)

Not a TypeError. With no ordering there is nothing to interpret, so the slice object is simply used as a key, and there is no such key.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).