Python itertools compress: Filtering with Boolean Masks
python itertools compress: Learn how itertools.compress() filters iterables using parallel boolean masks, with practical examples, comparison to filter(), and edge cases.
The python itertools compress function pairs each element of a data iterable with a selector from a second iterable and yields only the data elements whose selector is truthy. It is part of the standard library's itertools module, so no extra dependencies are required.
What compress() Does
itertools.compress(data, selectors) accepts two iterables and returns a lazy iterator. When you iterate over it, each element of data is paired with the corresponding element of selectors, and the data element is yielded only when the selector is truthy.
from itertools import compress data = ["alpha", "beta", "gamma", "delta"] selectors = [1, 0, 1, 0] print(list(compress(data, selectors))) # ['alpha', 'gamma']
The returned iterator is consumed lazily, so nothing is computed until you actually iterate. Both arguments can be any iterable: lists, tuples, generators, or even infinite streams.
How Selector Truthiness Works
Selectors are evaluated with Python's standard truthiness rules, not compared against True. Any value Python treats as false — 0, 0.0, "", [], None — suppresses the matching data element. Any truthy value includes it.
data = [10, 20, 30, 40] selectors = [0, 1, "", "yes"] print(list(compress(data, selectors))) # [20, 40]
This is convenient when your mask is already a list of integers or strings rather than explicit booleans. You do not need to normalize the selector values before passing them in.
Stopping at the Shortest Input
compress() stops as soon as either iterable is exhausted. If the selector list is shorter than the data, the remaining data elements are silently dropped.
data = [1, 2, 3, 4, 5] selectors = [1, 0, 1] print(list(compress(data, selectors))) # [1, 3]
The reverse also holds: if data is shorter than selectors, the extra selectors are ignored. This mirrors how zip() handles mismatched lengths, so the behavior is predictable once you know the rule.
Practical Use Cases
Filtering with a Precomputed Boolean Mask
The most common scenario is having a parallel list of booleans computed elsewhere. A list comprehension with zip() works, but compress() expresses the intent more directly.
import itertools records = [ {"id": 1, "status": "active"}, {"id": 2, "status": "archived"}, {"id": 3, "status": "active"}, ] mask = [r["status"] == "active" for r in records] active_records = list(itertools.compress(records, mask))
Combining with Other itertools Tools
compress() pairs naturally with functions that produce selector streams, such as cycle() or islice(). For example, you can select every other element by cycling a mask.
from itertools import compress, cycle data = [10, 20, 30, 40, 50] mask = cycle([1, 0]) print(list(compress(data, mask))) # [10, 30, 50]
compress() vs filter()
filter() applies a predicate function to each element and keeps the element when the predicate returns true. compress() instead uses a parallel iterable of selectors. The distinction matters when the decision about whether to keep an element is already encoded as data rather than computed from the element itself.
| Aspect | filter() | compress() |
|---|---|---|
| Selector type | Predicate function | Parallel iterable |
| Evaluation | Calls function per element | Checks selector truthiness |
| Termination | End of data | End of either input |
| Typical use | Value-based filtering | Mask-based filtering |
If the decision depends only on the element's own value, filter() or a comprehension is usually clearer. If the decision comes from a separate mask, compress() is the better fit.
Performance and Memory Behavior
Because compress() is lazy, it does not build an intermediate list. The selectors are consumed as you iterate, so a generator of selectors works without materializing the full mask.
def selector_stream(): for i in range(1000): yield i % 2 result = compress(range(1000), selector_stream())
This matters when the mask is expensive to compute or when the dataset is large enough that an extra list copy would be noticeable. The tradeoff is that the result is a one-shot iterator: if you need it more than once, you must convert it to a list or tuple.
Edge Cases and Limitations
An empty selector iterable produces an empty result regardless of the data. An infinite selector iterable paired with finite data terminates when the data runs out, so you do not need to slice the selectors to a matching length.
compress() never transforms values; it only selects them. If you need to map or reshape the selected elements, chain map() or a comprehension after it. It also does not accept a predicate function, so there is no way to compute the mask from the data element inside the call itself.
The one-shot nature of the returned iterator is the main practical limitation. If the same filtered sequence is needed in multiple places, materialize it once with list() and reuse that list.