Slicing & Unpacking

Two pieces of syntax show up all over idiomatic Python. Slicing extracts part of any sequence — a string, list, tuple or range. Unpacking pulls the items of a sequence out into separate variables, or spreads them into function calls and new collections.

Slice syntax

sequence[start:stop:step] returns the items from start up to but not including stop, taking every step-th item. Any part can be left out:

PY slices.py
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(nums[2:5])
print(nums[:3])
print(nums[7:])
print(nums[-3:])
print(nums[:-3])
print(nums[::3])
print(nums[1::2])
print(nums[::-1])
print(nums[8:2:-2])
Output
[2, 3, 4]
[0, 1, 2]
[7, 8, 9]
[7, 8, 9]
[0, 1, 2, 3, 4, 5, 6]
[0, 3, 6, 9]
[1, 3, 5, 7, 9]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[8, 6, 4]

A helpful mental model: indexes point between items. [2:5] cuts at the gaps before positions 2 and 5. That's why len(nums[a:b]) is b - a, and nums[:k] + nums[k:] always rebuilds the original.

Slices are forgiving

PY forgiving.py
letters = ["a", "b", "c"]
print(letters[1:100])
print(letters[5:])
print("hello"[10:20] == "")
Output
['b', 'c']
[]
True

Out-of-range slice bounds are clipped rather than raising an error, unlike single indexes.

Slices make copies

PY copy.py
original = [1, 2, 3]
copy = original[:]
copy.append(4)
print(original, copy)
Output
[1, 2, 3] [1, 2, 3, 4]

Assigning to slices

For lists, a slice on the left of = replaces that section — even with a different number of items. The exception is a slice with a step (an "extended slice" like [::2]), which must be given exactly as many items as it selects:

PY assign.py
letters = ["a", "b", "c", "d", "e"]

letters[1:3] = ["X", "Y", "Z"]
print(letters)

letters[:2] = []
print(letters)

letters[::2] = ["#", "#"]
print(letters)

del letters[-2:]
print(letters)
Output
['a', 'X', 'Y', 'Z', 'd', 'e']
['Y', 'Z', 'd', 'e']
['#', 'Z', '#', 'e']
['#', 'Z']

Named slices

When parsing fixed-width data, slice() objects give positions meaningful names:

PY named.py
record = "2026-10-06 ERROR disk full"
DATE = slice(0, 10)
LEVEL = slice(11, 16)
MESSAGE = slice(17, None)

print(record[DATE], "|", record[LEVEL], "|", record[MESSAGE])
Output
2026-10-06 | ERROR | disk full

Unpacking

PY unpack.py
point = (3, 4)
x, y = point

name, *middle, last = ["Ana", "Maria", "Silva", "Costa"]
first, *_ = "Python"

print(x, y)
print(name, middle, last)
print(first)
Output
3 4
Ana ['Maria', 'Silva'] Costa
P

The number of names must match the number of items, unless one name is starred — that one collects everything left over into a list. _ is the conventional name for values you don't need.

PY mismatch.py
a, b = [1, 2, 3]
Output
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    a, b = [1, 2, 3]
    ^^^^
ValueError: too many values to unpack (expected 2, got 3)

Nested unpacking and loops

PY nested.py
people = [("Ana", (38.7, -9.1)), ("Ben", (41.1, -8.6))]

for name, (lat, lon) in people:
    print(f"{name}: {lat}°N, {abs(lon)}°W")

for index, (key, value) in enumerate({"a": 1, "b": 2}.items()):
    print(index, key, value)
Output
Ana: 38.7°N, 9.1°W
Ben: 41.1°N, 8.6°W
0 a 1
1 b 2

Unpacking into calls and collections

A single * spreads a sequence into separate positional arguments or items; ** spreads a dictionary into keyword arguments or entries:

PY spread.py
def describe(name, age, city):
    return f"{name}, {age}, from {city}"

args = ["Ana", 31, "Lisbon"]
kwargs = {"name": "Ben", "age": 25, "city": "Porto"}

print(describe(*args))
print(describe(**kwargs))

numbers = [1, 2, 3]
print([0, *numbers, 4], (*numbers, 9), {*numbers, 3})

base = {"a": 1}
print({**base, "b": 2})
print(*numbers, sep=" < ")
Output
Ana, 31, from Lisbon
Ben, 25, from Porto
[0, 1, 2, 3, 4] (1, 2, 3, 9) {1, 2, 3}
{'a': 1, 'b': 2}
1 < 2 < 3

Try it yourself

Try it yourself
Console output