Python Built-in Iteration Functions

Explore Python's built-in functions for working with iterators, including map, filter, zip, enumerate, reversed, sorted, and the powerful itertools module.

6 min read

Python ships with a collection of built-in functions designed specifically for working with iterators and iterables. Functions like map, filter, zip, enumerate, and reversed are available in every Python program without any imports, and they all return lazy iterator objects that produce values on demand. When you combine these with the itertools module from the standard library, which adds dozens more iterator-based building blocks, you get a complete vocabulary for expressing data transformations without writing explicit for loops.

If you have internalized the concept of lazy evaluation in Python, you already understand why these functions return iterators instead of lists. Each function sets up a computation that will happen later, when a consumer starts pulling values through it. The map() function does not apply a transformation to every element immediately. It returns a map object that will apply the transformation to each element as it is requested. This design means you can chain map, filter, zip, and other iterator-producing functions into pipelines that process data in a single pass with no intermediate storage.

The itertools module extends this vocabulary with specialised iterators for slicing, grouping, combining, and cycling through data. Every function in itertools returns an iterator, and every one is implemented in C for performance. They are the building blocks that experienced Python developers reach for instead of writing nested for loops with accumulator lists. A single line of itertools can replace a five-line loop, and it usually runs faster because the iteration logic executes in C rather than in Python.

map(): transforming every element

The map() function applies a callable to every element of an iterable and yields the results one at a time. It is the functional programming alternative to a list comprehension with a transformation. Where a list comprehension would create an entire list, map() returns a lazy iterator that computes each transformed value only when it is requested:

pythonpython
numbers = [1, 2, 3, 4, 5]
squared = map(lambda x: x * x, numbers)

The variable squared is a map iterator. No squaring has happened yet. When you iterate over squared with a for loop or pass it to list(), the lambda runs once per element, and the results are produced on demand. You can pass multiple iterables to map(), and the callable will receive one argument from each iterable on every call. The iterator stops when the shortest input iterable is exhausted.

For simple transformations like squaring a number, a generator expression is often more readable than map() with a lambda. The expression (x * x for x in numbers) reads more naturally to most Python developers than map(lambda x: x * x, numbers). But map() shines when the callable already exists as a named function. Writing map(str.upper, names) is clearer than (name.upper() for name in names) because it names the operation directly without introducing a loop variable. The choice between map() and a generator expression is largely stylistic, and both produce equivalent lazy iterators.

filter(): keeping elements that pass a test

The filter() function applies a predicate, a callable that returns True or False, to each element of an iterable and yields only the elements for which the predicate returns True. Like map(), filter() is lazy: it tests each element only when the value is requested, and it never builds an intermediate list of passing elements:

pythonpython
def is_even(x):
    return x % 2 == 0
 
evens = filter(is_even, range(20))

If you pass None as the predicate instead of a callable, filter() yields elements that are truthy, effectively removing falsy values like zero, empty strings, and None from the iterable. This is a quick way to strip out empty or missing values from a dataset, though you should use it carefully because it also removes legitimate zero values, which may or may not be what you intend.

As with map(), the readability of filter() compared to a generator expression depends on context. The expression (x for x in numbers if x % 2 == 0) is usually clearer than filter(lambda x: x % 2 == 0, numbers), but filter(str.isdigit, strings) reads well because the predicate is a named method. The same guideline applies: use filter() when the predicate is a named function or method, and use a generator expression when the predicate is a short inline expression.

zip() and enumerate(): combining and indexing

The zip() function takes multiple iterables and yields tuples where the first tuple contains the first element from each iterable, the second tuple contains the second element from each, and so on. It stops when the shortest input iterable is exhausted. This is the standard way to iterate over two or more sequences in parallel without managing index variables:

pythonpython
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
for name, score in zip(names, scores):
    print(f"{name}: {score}")

The enumerate() function wraps an iterable and yields pairs of an incrementing index and the original value. It replaces the common pattern of maintaining a counter variable alongside a for loop. The start parameter lets you control the initial index value, which is useful when you need one-based numbering for display to users or when you are numbering items in a report:

pythonpython
for position, item in enumerate(tasks, start=1):
    print(f"{position}. {item}")

Both zip() and enumerate() return lazy iterators. They do not create lists of tuples. Each tuple is constructed on demand when the consumer requests the next value, which means you can zip together iterators that each produce millions of values without allocating memory for millions of tuples simultaneously.

The itertools module

The itertools module contains three categories of iterator functions: infinite iterators that produce values forever, finite iterators that terminate based on their input, and combinatoric iterators that generate permutations and combinations. All are lazy, all are implemented in C, and all return iterator objects.

The infinite iterators, count(), cycle(), and repeat(), are useful for generating sequences of values where the consumer decides when to stop. The count() function acts like an unbounded range, producing integers from a starting value forever. The cycle() function repeats an iterable indefinitely. The repeat() function yields the same value forever or a specified number of times. You typically consume infinite iterators with islice() to take a fixed number of values:

pythonpython
from itertools import islice, count, cycle
first_ten = list(islice(count(10, 2), 10))
repeated = list(islice(cycle(["on", "off"]), 6))

The finite iterators are the workhorses of data processing pipelines. The islice() function slices any iterable lazily, like list slicing but without materializing the list. The chain() function concatenates multiple iterables into a single lazy sequence. The groupby() function groups consecutive elements that share a key, yielding groups as iterators. The accumulate() function produces running totals, running products, or any running accumulation defined by a custom function. The pairwise() function, added in Python 3.10, yields overlapping pairs of consecutive elements.

These functions replace common loop patterns with declarative calls that express intent directly. Instead of writing a loop that tracks a running total in a variable and appends to a list, you call accumulate() and iterate over the result. Instead of nested loops that walk through multiple lists sequentially, you call chain() and iterate once. The code becomes shorter, clearer, and often faster because the iteration logic runs in C.

Composing built-in functions into pipelines

The real expressiveness of Python's iteration functions comes from composing them. You pass the output of one function as the input to the next, building a pipeline that reads like a description of the data flow. A pipeline that extracts even numbers, squares them, and sums the result can be written as a chain of calls:

pythonpython
from itertools import islice
nums = range(100)
even = filter(lambda x: x % 2 == 0, nums)
squared = map(lambda x: x * x, even)
result = sum(islice(squared, 10))

Each intermediate variable is a lazy iterator. No computation happens until sum() starts pulling values. When it does, the request propagates backward through the pipeline: sum asks squared for a value, squared asks even for a value, even asks nums for a value. Only when an even number is found does the square get computed and passed to sum. This pull-based model is the essence of lazy pipeline composition.

The readability of composed pipelines depends on naming. When each intermediate step is assigned to a well-named variable, as in the example above, the pipeline tells a story. When all the steps are nested in a single expression, readability suffers. The art of writing clear pipeline code is finding the right balance between chaining and naming. If a pipeline is used once and is short, inlining is fine. If it is used in multiple places, extract it into a generator function with a descriptive name.

Rune AI

Rune AI

Key Insights

  • map(), filter(), zip(), enumerate(), and reversed() all return lazy iterators in Python 3, enabling pipeline composition without intermediate lists.
  • The itertools module provides building blocks for slicing, chaining, grouping, combining, and cycling through iterators.
  • Chaining these functions replaces nested for loops with declarative, readable pipelines.
  • All itertools functions are lazy and use constant memory regardless of input size.
  • For loops are still the right tool for complex logic; built-in iteration functions shine when the transformation is simple and composable.
RunePowered by Rune AI

Frequently Asked Questions

Why do map() and filter() return iterators instead of lists in Python 3?

Python 3 changed map() and filter() to return lazy iterators instead of lists to save memory and enable pipeline composition. In Python 2, map() and filter() built complete lists in memory, which was wasteful when the result was immediately consumed by a for loop or passed to another function. The Python 3 behaviour means you can chain map(), filter(), and other iterator-producing functions without creating intermediate lists. If you need a list, you can wrap the result in list() explicitly.

What is the difference between zip() and itertools.zip_longest()?

The built-in zip() function stops when the shortest input iterable is exhausted, silently dropping any remaining items from longer iterables. The itertools.zip_longest() function continues until the longest input is exhausted, filling in missing values with a specified fillvalue, which defaults to None. Use zip() when you expect all inputs to have the same length. Use zip_longest() when you need to preserve all items from all inputs and you have a sensible default for missing values.

When should I use itertools instead of writing my own generator?

Use itertools when the pattern you need matches one of the module's functions: islice for slicing, chain for concatenation, groupby for grouping, combinations and permutations for combinatorics, cycle and repeat for infinite iteration, and accumulate for running totals. The itertools functions are implemented in C and are faster than equivalent Python generators. They also communicate intent clearly: a reader who sees islice() knows exactly what is happening without parsing a custom loop.

Conclusion

Python's built-in iteration functions and the itertools module give you a rich vocabulary for working with iterators. They replace explicit loops with declarative function calls, enable lazy pipeline composition, and are implemented in C for performance. Learning these functions changes the way you write Python: instead of reaching for a for loop and an accumulator list, you reach for map, filter, zip, and the itertools that express your intent directly.