Module 9 · Idiomatic & Best Practices ⏱ 18 min

Pythonic Patterns & Idioms

By the end of this lesson you will be able to:
  • Replace range(len(...)) loops with enumerate to pair each item with its index
  • Use zip to walk two sequences in parallel and build dictionaries in one line
  • Swap values and unpack sequences idiomatically, and avoid zip's silent truncation trap

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 hands you the index and the value together — no separate lookup.

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.

enumerate yields (index, value) pairs as it walks the list. Press Run.
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
zip walks two lists in parallel and stops at the shorter one.
zip pairs element-wise; the longer list is truncated silently.
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 two-value swap and a three-value rotation, each in one line.
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
A swap packs the right side into a tuple, then unpacks it into the names on the left.

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.

Exercise

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
Exercise

Two values are swapped in a single line. Predict the output.

a, b = 1, 2
a, b = b, a
print(a, b)
Exercise

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
Exercise

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
Exercise

What does list(zip([1, 2, 3], ['a', 'b'])) return?

Recap

  • Use enumerate instead of range(len(...)) to loop over index and value together.
  • Use zip to 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 with if 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.

Checkpoint quiz

Which is the pythonic way to loop over a list with both index and value?

What does dict(zip(['a', 'b'], [1, 2])) produce?

Go deeper — technical resources