Skip to content

enumerate, zip & Unpacking Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on enumerate with start, zip and zip_longest, unzipping with zip(*), extended iterable unpacking, and star-args at the call site.

Read the in-depth guidePython enumerate, zip and Unpacking Explained — Indexes, Pairing, and Star Args(opens in new tab)
15 of 15

enumerate(iterable) pairs each item with a running index, yielding (index, value) tuples. It saves you from the error-prone manual counter or range(len(...)) pattern. The optional start argument sets the first index (default 0).

colors = ["red", "green", "blue"]

for i, c in enumerate(colors):
    print(i, c)            # 0 red / 1 green / 2 blue

for rank, c in enumerate(colors, start=1):
    print(rank, c)         # 1 red / 2 green / 3 blue

Prefer enumerate over range(len(seq)) whenever you need both the index and the item — it's more readable and works on any iterable, not just indexable sequences.

zip(a, b, ...) walks several iterables in parallel, yielding tuples of aligned elements. It stops at the shortest input, silently dropping extras. itertools.zip_longest instead runs to the longest, filling missing slots with a fillvalue.

names = ["ada", "grace", "edsger"]
ages  = [36, 45]

list(zip(names, ages))     # [('ada', 36), ('grace', 45)]  — 'edsger' dropped

from itertools import zip_longest
list(zip_longest(names, ages, fillvalue=0))
# [('ada', 36), ('grace', 45), ('edsger', 0)]

Use plain zip when lengths match (or truncation is intended); reach for zip_longest when you must not lose data from the longer iterable.

zip is its own inverse. Applying zip(*pairs) unpacks the list of tuples as separate arguments, so zip re-groups them column-wise — effectively transposing rows into columns.

pairs = [("ada", 36), ("grace", 45)]

names, ages = zip(*pairs)
names    # ('ada', 'grace')
ages     # (36, 45)

This also transposes a matrix: list(zip(*matrix)). Note the results are tuples, not lists, and the input is consumed if it's an iterator — wrap in list(...) if you need list results.

A starred target in an assignment captures "the rest" as a list. You can place *name at the start, middle, or end, and the non-starred names take fixed positions — Python figures out the split.

first, *rest = [1, 2, 3, 4]      # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4]       # init=[1, 2, 3], last=4
head, *mid, tail = [1, 2, 3, 4]  # head=1, mid=[2, 3], tail=4

Exactly one starred target is allowed per assignment, and it always becomes a list (even if empty). This is cleaner than slicing for grabbing the head/tail of a sequence.

Pairing two sequences with zip and feeding them to dict() is the idiomatic way to build a mapping. The * and ** operators also unpack iterables/dicts into function calls as positional and keyword arguments.

keys = ["x", "y", "z"]
vals = [1, 2, 3]
d = dict(zip(keys, vals))        # {'x': 1, 'y': 2, 'z': 3}

def point(x, y, z): return (x, y, z)
args = [1, 2, 3]
point(*args)                     # unpack list -> positional
point(**d)                       # unpack dict -> keyword args

Use * to spread a sequence into positional parameters and ** to spread a dict into keyword parameters — the call-site mirror of *args/**kwargs in a function signature.

In Python 3.10+, zip(a, b, strict=True) raises ValueError if the iterables have different lengths, instead of silently truncating. It catches a class of bugs where you assumed two sequences were aligned.

list(zip([1, 2, 3], ["a", "b"]))                 # [(1,'a'),(2,'b')] — silent loss
list(zip([1, 2, 3], ["a", "b"], strict=True))    # ValueError — length mismatch

Rule of thumb: pass strict=True when equal lengths are an invariant — fail loudly rather than silently dropping data from the longer iterable.

enumerate gives you the index and the item directly, works on any iterable (not just indexable sequences), and avoids repeated seq[i] lookups. range(len()) is verbose, error-prone, and breaks on generators.

# clunky and index-only:
for i in range(len(colors)):
    print(i, colors[i])

# clear, item in hand, works on any iterable:
for i, c in enumerate(colors):
    print(i, c)

Rule of thumb: if you find yourself writing range(len(x)), you almost always want enumerate(x) instead.

zip shines inside comprehensions to combine aligned sequences element-wise — summing pairs, building records, or comparing columns — in one readable expression.

prices = [10, 20, 30]
qtys   = [2, 1, 5]

totals = [p * q for p, q in zip(prices, qtys)]   # [20, 20, 150]
records = [{"price": p, "qty": q} for p, q in zip(prices, qtys)]
grand = sum(p * q for p, q in zip(prices, qtys)) # 190

Rule of thumb: for a, b in zip(xs, ys) inside a comprehension is the idiom for element-wise combination of parallel sequences.

You can destructure nested structure directly in the loop target, mirroring the shape of each item. This is common with enumerate over pairs or zipped tuples.

pairs = [(1, ("a", "b")), (2, ("c", "d"))]
for num, (left, right) in pairs:
    print(num, left, right)     # 1 a b / 2 c d

for i, (name, age) in enumerate(zip(names, ages)):
    ...                         # index + destructured pair

Rule of thumb: match the loop target's shape to the item's shape — nested parentheses unpack nested tuples without manual indexing.

Yes — you can interleave unpacked iterables with explicit positional args, and even use multiple * unpackings in one call (3.5+). The same applies to ** for keywords. They're expanded left to right.

def f(a, b, c, d): return (a, b, c, d)

pair = [2, 3]
f(1, *pair, 4)            # (1, 2, 3, 4) — star in the middle
f(*[1, 2], *[3, 4])       # (1, 2, 3, 4) — multiple unpackings

d1 = {"a": 1}; d2 = {"b": 2}
dict(**d1, **d2)          # {'a': 1, 'b': 2} — merge via **

Rule of thumb: */** can be combined with literals and each other in a call; Python flattens them positionally/by-keyword in order.

Since 3.5, * unpacks iterables into list/tuple/set literals and ** unpacks dicts into dict literals — a clean way to merge or extend without +, update, or extend.

a = [1, 2]; b = [3, 4]
[*a, *b, 5]              # [1, 2, 3, 4, 5]
{*a, *b}                # {1, 2, 3, 4}

d1 = {"x": 1}; d2 = {"y": 2}
{**d1, **d2, "z": 3}    # {'x': 1, 'y': 2, 'z': 3}  — later keys win

Rule of thumb: [*a, *b] and {**d1, **d2} are the idiomatic literal-merge forms; with dicts, rightmost duplicate keys override earlier ones.

Yes — in Python 3 zip returns a lazy iterator, not a list. It produces tuples on demand and is single-pass: once consumed, it's exhausted. Wrap in list() to materialize or reuse.

z = zip([1, 2], ["a", "b"])
list(z)        # [(1, 'a'), (2, 'b')]
list(z)        # []  — already exhausted!

pairs = list(zip(xs, ys))   # materialize if you need it twice

Rule of thumb: zip is a one-shot iterator — convert to a list if you must iterate more than once or index into it.

Yes — enumerate accepts any iterable, including generators, files, and zip/map objects, because it only pulls items one at a time. This is a key advantage over range(len()), which needs a sized, indexable sequence.

with open("data.txt") as f:
    for lineno, line in enumerate(f, start=1):   # works on a file iterator
        print(lineno, line.rstrip())

for i, sq in enumerate(n * n for n in range(5)):  # works on a generator
    ...

Rule of thumb: enumerate is universal across iterables; len-based indexing is not — prefer enumerate for streaming sources.

a, b = b, a works because the right side is evaluated into a tuple first, then unpacked into the targets. No temporary variable is needed, and it generalizes to any number of names.

a, b = 1, 2
a, b = b, a            # a=2, b=1

x, y, z = 1, 2, 3
x, y, z = z, x, y      # rotate: x=3, y=1, z=2

# common in algorithms (e.g. Fibonacci):
a, b = b, a + b

Rule of thumb: the right-hand side is fully evaluated before assignment, so multi-target swaps/rotations are safe and atomic-looking.

Plain zip silently truncates to the shortest iterable, so mismatched lengths lose data without any warning. If one source is an iterator, zip also consumes one extra element from the longer one while detecting the stop.

a = iter([1, 2, 3])
b = [10, 20]
list(zip(a, b))    # [(1, 10), (2, 20)]
list(a)            # [] or [3]? — 3 may already be consumed by zip

Rule of thumb: when lengths should match, use strict=True; when they shouldn't, use zip_longest — don't rely on silent truncation, and beware consuming iterators.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel