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, ...).