There is a difference between code that Python will run and code that reads the way an experienced Python programmer thinks. The first kind works; the second kind is pythonic — it leans on the tools the language already provides instead of rebuilding them by hand.
A tell-tale sign of un-pythonic code is a loop wrestling with indexes: for i in range(len(items)) just to reach both the position and the value, or a temp variable shuttling two values around to swap them. Each works, and each is a small signal that the author brought habits from another language.
Python ships enumerate, zip, and tuple unpacking precisely to make these patterns disappear. This lesson shows the three idioms you will reach for every day, and the quiet trap that catches everyone the first time they pair two lists of different lengths.
flowchart LR
subgraph U["un-pythonic"]
A1["for i in range(len(names))"] --> A2["then names[i]"]
end
subgraph P["pythonic"]
B1["for i, name in enumerate(names)"] --> B2["both at once"]
end
style A1 fill:#b45309,color:#fff
style B1 fill:#3776ab,color:#fff
enumerate: index and value together
The pattern for i in range(len(items)) is so common that Python ships a direct replacement. enumerate hands you each item together with its position, so you never index back into the list:
for i, name in enumerate(names):
print(i, name)
enumerate yields pairs, and tuple unpacking splits each pair into i and name in one step. It also takes an optional start argument — enumerate(names, start=1) numbers from one — which is exactly what you want when printing a numbered menu.
The payoff is not just brevity. Removing the index arithmetic removes a whole class of off-by-one mistakes, because there is no longer a separate counter to forget to increment.
names = ["ada", "linus", "guido"]
for i, name in enumerate(names):
print(i, name)
zip: pairing two sequences
When you have two lists that line up — names and scores, products and prices — zip walks them side by side and hands you one element from each:
for name, score in zip(names, scores):
print(name, score)
zip stops as soon as the shortest sequence ends, which is usually what you want and occasionally a quiet surprise. When the lists are the same length it is invisible; when they are not, the extra items in the longer list are dropped without a warning.
zip pairs beautifully with dict construction. dict(zip(keys, values)) builds a dictionary in a single readable line, which is far clearer than looping and assigning each key by hand.
flowchart LR N["names: ada, linus, guido"] --> Z["zip"] SC["scores: 90, 80"] --> Z Z --> P["(ada,90) (linus,80)<br/>guido dropped"] style Z fill:#3776ab,color:#fff
names = ["ada", "linus", "guido"] scores = [90, 80] print(list(zip(names, scores))) print(dict(zip(["a", "b"], [1, 2])))
Tuple unpacking and the swap
Python lets you assign to several names at once by unpacking a sequence — first, second = [10, 20] sets both in one statement. The same mechanism makes swapping two values a single line with no temporary variable:
a, b = b, a
Read it as: evaluate the right-hand side as a tuple, then unpack it into the names on the left. The swap is atomic and obvious, and it generalises — a, b, c = c, a, b rotates three values with no bookkeeping at all.
Unpacking also pulls apart function results. A function that does return x, y is returning a tuple, and width, height = get_size() receives it cleanly.
a, b = 1, 2 a, b = b, a print(a, b) x, y, z = 1, 2, 3 x, y, z = z, x, y print(x, y, z)
flowchart LR R["right side: (b, a)"] --> P["packed into a tuple"] P --> U["unpacked into a, b"] U --> F["a and b are swapped"] style P fill:#3776ab,color:#fff
Iterating dictionaries the pythonic way
A common beginner mistake is looping over a dictionary with for key in d and then looking up d[key] inside the body. It works, but it does an extra lookup on every step. d.items() hands you each key and value together, ready to unpack:
for word, count in counts.items():
print(word, count)
The same shape applies to enumerate, zip, and items: ask for everything you need in the for line, and let unpacking hand it to you. That habit — naming the parts up front instead of fetching them later — is the thread that runs through every pythonic loop.
Truthiness: let Python decide
Python can test most values directly in an if. An empty list, string, or dict is falsy; a non-empty one is truthy. So instead of if len(items) > 0:, the pythonic form is simply if items: — and instead of if len(items) == 0:, write if not items:.
This reads as English and works for every container at once. It is the same idea as the other idioms here: trust the language to do the obvious thing, and your code shrinks to what you actually mean.
Write numbered(items) that returns a list of (index, item) pairs for the given list, using enumerate. So numbered(['a', 'b']) gives [(0, 'a'), (1, 'b')].
def numbered(items):
# use enumerate and list()
pass
enumerate(items) yields (index, item) pairs.
Wrap it in list() to collect them into a list.
def numbered(items):
return list(enumerate(items))
Two values are swapped in a single line. Predict the output.
a, b = 1, 2 a, b = b, a print(a, b)
The right side (b, a) is (2, 1) before the assignment.
Unpacking assigns those values back into a and b.
This is meant to return a list of (index, name) pairs, but every pair carries the wrong index — the counter never advances and it starts at 1. Replace the hand-rolled counter with enumerate so each pair carries its real position.
def indexed(names):
result = []
i = 1
for name in names:
result.append((i, name))
return result
enumerate(names) yields (0, name), (1, name), and so on.
list(enumerate(names)) builds the whole list at once.
def indexed(names):
return list(enumerate(names))
Write pair_up(keys, values) that returns a dictionary mapping each key to its paired value, using zip. So pair_up(['a', 'b'], [1, 2]) gives {'a': 1, 'b': 2}.
def pair_up(keys, values):
# zip + dict
pass
zip(keys, values) yields ('a', 1), ('b', 2).
dict() turns those pairs into a dictionary.
def pair_up(keys, values):
return dict(zip(keys, values))
What does list(zip([1, 2, 3], ['a', 'b'])) return?
zip stops at the shortest input. The second list has two items, so only the first two of the first list are paired — the 3 is dropped silently, with no padding and no error.
Recap
- Use
enumerateinstead ofrange(len(...))to loop over index and value together. - Use
zipto walk two sequences in parallel; remember it stops at the shortest. - Tuple unpacking swaps values in one line —
a, b = b, a— with no temporary variable. - Loop over a dict with
.items()to get key and value at once, and test containers directly withif items:.
Next you will learn to look at a loop and judge not just whether it works, but how it scales when the input grows a hundredfold.