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:
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])
[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
letters = ["a", "b", "c"] print(letters[1:100]) print(letters[5:]) print("hello"[10:20] == "")
['b', 'c'] [] True
Out-of-range slice bounds are clipped rather than raising an error, unlike single indexes.
Slices make copies
original = [1, 2, 3]
copy = original[:]
copy.append(4)
print(original, copy)
[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:
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)
['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:
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])
2026-10-06 | ERROR | disk full
Unpacking
point = (3, 4) x, y = point name, *middle, last = ["Ana", "Maria", "Silva", "Costa"] first, *_ = "Python" print(x, y) print(name, middle, last) print(first)
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.
a, b = [1, 2, 3]
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
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)
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:
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=" < ")
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