Each user's permissions are a list of bit flags, and you want one combined
mask. The functools.reduce function combines a list into one value by
calling a two-argument function over and over:
from functools import reduce
from operator import or_
masks = [1, 4, 2]
print(reduce(or_, masks, 0)) # 7
The operator module has a function for each operator, and or_ is the
function form of |. Starting from 0, reduce computes or_(0, 1), then
or_(1, 4), then or_(5, 2). Each step passes the running result and the next
item, from left to right. So the order matters for an operation such as
subtraction:
from operator import sub
print(reduce(sub, [10, 3, 2])) # 5, from (10 - 3) - 2
print(reduce(sub, [10, 3, 2], 20)) # 5, from ((20 - 10) - 3) - 2
Use a built-in when one fits
Most uses of reduce have a clearer built-in:
| Goal | Use |
|---|---|
| Add numbers | sum(values) |
| Multiply numbers | math.prod(values) |
| Find the smallest or largest | min(values), max(values) |
| Check truth values | any(values), all(values) |
| Join strings | "".join(parts) |
| Flatten one level of lists | list(chain.from_iterable(groups)) |
| Merge dictionaries, later values winning | A loop that calls update |
Some built-ins also do less work. The any and all functions stop at
the first item that decides the answer, while reduce always reads the whole
input. If the combining step needs several statements, an early exit or error
handling, write a loop.
Decide what an empty list means
Without a starting value, reduce uses the first item as the start. A list
with one item gives that item back unchanged, and an empty list raises
TypeError. With a starting value, an empty list gives back the starting
value:
empty: list[int] = []
print(reduce(or_, empty, 0)) # 0
Pick a starting value that leaves the result unchanged, such as 0 for | and
+, or 1 for *. If an empty list means a bug in your program, leave the
starting value out and let reduce raise. Do not invent a default only to
avoid the error.
The starting value can also have a different type from the items:
def append_label(text: str, value: int) -> str:
return f"{text}[{value}]"
print(reduce(append_label, [1, 2, 3], "items:")) # items:[1][2][3]
Here the running result is a str and each item is an int, and mypy checks
that the function and the starting value agree.
Adding lists copies them again and again
On a list of lists, reduce(operator.add, chunks) builds a new list at every
step and copies everything collected so far into it. With 1,000 chunks of 10
items, the last step alone copies about 10,000 items, and the whole call copies
about 5 million. Flatten in one pass instead:
from itertools import chain
chunks = [[1, 2], [3], [], [4, 5]]
print(list(chain.from_iterable(chunks))) # [1, 2, 3, 4, 5]
That visits each item once, and so does a loop that calls extend on one list.
Building a long string with + has the same problem, so use "".join(parts).
A call such as sum(chunks, []) copies the same way reduce does.
Counter addition drops counts
You met Counter in the first article of this track. Adding two Counter
objects with + throws away zero and negative counts at every step. The
update method adds counts in place and keeps them. So switching from one to
the other can change the answer:
from collections import Counter
from operator import add
counters = [Counter({"x": -2}), Counter({"x": 3})]
added = reduce(add, counters, Counter[str]())
total: Counter[str] = Counter()
for counter in counters:
total.update(counter)
print(added, total) # Counter({'x': 3}) Counter({'x': 1})
For counts that are never negative, the two agree, and update avoids
building a new Counter at each step. When negative counts mean something,
decide whether to keep them before you pick one.
Check your understanding
A call to reduce(operator.add, list_of_lists) works in your tests but is slow on a million chunks. Why, and what should you use instead?
Show answer
Hide answer
Each + builds a new list and copies everything collected so far, so the
copying grows with the square of the number of chunks. Use
list(chain.from_iterable(list_of_lists)), or a loop that calls extend on
one list. Both copy each item once.
Going further
Since Python 3.14, you can pass the starting value by keyword, as initial=0.
Code that must also run on 3.12 passes it by position.