Iteration Utilities: enumerate and zip

Updated

September 7, 2026

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.py

Output:

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.py

Output:

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

  1. Use enumerate(lines, start=1) to format an error report that prints the line number and content for lines containing "FATAL".
  2. Given a list of keys and a list of values, use zip(keys, values, strict=True) inside a dictionary comprehension to build a map.
  3. Demonstrate why zip() stops early when one argument is an infinite generator (like itertools.count()) and the other is a finite list.
  4. 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.