Iteration Utilities: enumerate and zip
Iteration Utilities: enumerate and zip
After reading this chapter, you will master Python’s built-in iteration helpers: track loop indices cleanly without manual counter variables using enumerate(), iterate multiple parallel streams simultaneously using zip(), enforce length symmetry using strict=True, and handle unequal lengths with itertools.zip_longest().
Mental model
In low-level languages, iterating across parallel arrays or tracking line numbers requires manual integer incrementation (i++). Python replaces manual counters with lazy iterator wrappers:
Manual Index Management (Error-Prone):
i = 0
for item in items:
print(i, item)
i += 1 ──▶ Easy to forget, off-by-one errors
enumerate(items, start=1) Pipeline:
items: [ "web", "api", "db" ]
│
▼
Yields: (1, "web") ──▶ (2, "api") ──▶ (3, "db") (Zero overhead)
Multi-Stream Parallel Iteration (zip):
Stream 1: [ "node-1", "node-2", "node-3" ]
Stream 2: [ "10.0.1.1", "10.0.1.2", "10.0.1.3" ]
│ │ │
▼ ▼ ▼
zip: ("node-1", ("node-2", ("node-3",
"10.0.1.1") "10.0.1.2") "10.0.1.3")
Minimal example
Save as iteration_utilities.py:
# iteration_utilities.py
def main() -> None:
# 1. Indexed iteration with enumerate(start=1)
services = ["auth-gateway", "billing-worker", "orders-api"]
print("--- Numbered Service Manifest ---")
for index, service in enumerate(services, start=1):
print(f"Service #{index:02d}: {service}")
# 2. Parallel lock-step iteration with zip(..., strict=True)
hosts = ["node-01", "node-02", "node-03"]
ips = ["10.0.1.10", "10.0.1.11", "10.0.1.12"]
roles = ["primary", "replica", "replica"]
print("\n--- Synchronized Cluster Inventory ---")
# strict=True guarantees ValueError if any list length differs
for host, ip, role in zip(hosts, ips, roles, strict=True):
print(f"Host: {host:8} | IP: {ip:10} | Role: {role}")
if __name__ == "__main__":
main()Run via uv run python iteration_utilities.py:
--- Numbered Service Manifest ---
Service #01: auth-gateway
Service #02: billing-worker
Service #03: orders-api
--- Synchronized Cluster Inventory ---
Host: node-01 | IP: 10.0.1.10 | Role: primary
Host: node-02 | IP: 10.0.1.11 | Role: replica
Host: node-03 | IP: 10.0.1.12 | Role: replica
Worked examples
Case 1: Catching Silent Data Truncation with strict=True
In Python 3.9 and earlier, zip() silently stopped iterating as soon as the shortest sequence was exhausted, dropping trailing elements without any error. PEP 618 introduced strict=True:
# strict_zip_verification.py
def verify_cluster_mapping(hostnames: list[str], ip_addresses: list[str]) -> None:
try:
# If one list has 3 items and the other has 2, strict=True raises ValueError
for host, ip in zip(hostnames, ip_addresses, strict=True):
print(f"Mapping: {host} -> {ip}")
except ValueError as exc:
print(f"CRITICAL DRIFT: Configuration mismatch detected: {exc}")
if __name__ == "__main__":
configured_hosts = ["web-01", "web-02", "web-03"]
assigned_ips = ["192.168.1.10", "192.168.1.11"] # Missing 3rd IP!
print("Attempting to pair hosts and IPs with strict=True:")
verify_cluster_mapping(configured_hosts, assigned_ips)Run:
uv run python strict_zip_verification.pyOutput:
Attempting to pair hosts and IPs with strict=True:
Mapping: web-01 -> 192.168.1.10
Mapping: web-02 -> 192.168.1.11
CRITICAL DRIFT: Configuration mismatch detected: zip() argument 2 is shorter than argument 1
Case 2: Unequal Streams with Padding Using itertools.zip_longest
When sequence lengths differ intentionally and you wish to fill missing values with a placeholder, use itertools.zip_longest():
# zip_longest_demo.py
import itertools
def display_table_with_padding() -> None:
headers = ["Service", "Primary Host", "Backup Host"]
col1 = ["auth", "billing"]
col2 = ["auth-01", "bill-01", "order-01"]
col3 = ["auth-bak"]
print("Padded multi-column output:")
for svc, primary, backup in itertools.zip_longest(col1, col2, col3, fillvalue="[NONE]"):
print(f" {svc:10} | Primary: {primary:10} | Backup: {backup:10}")
if __name__ == "__main__":
display_table_with_padding()Run:
uv run python zip_longest_demo.pyOutput:
Padded multi-column output:
auth | Primary: auth-01 | Backup: auth-bak
billing | Primary: bill-01 | Backup: [NONE]
[NONE] | Primary: order-01 | Backup: [NONE]
Pitfalls
Pitfall 1: Relying on Default zip() Without strict=True
Omitting strict=True in production code causes subtle bugs where data streams (like headers vs rows in CSVs, or keys vs values) fail to match, silently corrupting or discarding records. Always write zip(..., strict=True).
Pitfall 2: Re-Iterating a zip or enumerate Object
Both zip() and enumerate() return single-pass iterators. Once consumed, iterating over them a second time yields 0 items:
pairs = zip([1, 2], ["a", "b"])
print(list(pairs)) # [(1, 'a'), (2, 'b')]
print(list(pairs)) # [] (Exhausted!)
# If you need to re-use the pairs, convert to a list:
pairs_list = list(zip([1, 2], ["a", "b"]))Exercises
- Use
enumerate(lines, start=1)to format an error report that prints the line number and content for lines containing"FATAL". - Given a list of keys and a list of values, use
zip(keys, values, strict=True)inside a dictionary comprehension to build a map. - Demonstrate why
zip()stops early when one argument is an infinite generator (likeitertools.count()) and the other is a finite list. - Implement a matrix transpose operation using
zip(*matrix).
Further reading
- PEP 618: Add Optional Length-Checking To zip.
- Python Standard Library:
builtins.enumerate,builtins.zip. - Python Standard Library:
itertools.zip_longest.