2025, Dec 12 13:00
Generate x, f(x), f(f(x)) in Python with a clean one-liner: list comprehension + walrus operator
Learn a Python pattern to build x, f(x), f(f(x)) sequences: a list comprehension with walrus operator, avoiding extra calls and awkward itertools.accumulate.
Generating the sequence x, f(x), f(f(x)), ... is a common micro-pattern in Python, but the “obvious” approaches often come with trade-offs: an unnecessary extra function call, a clumsy misuse of functional helpers, or naming pitfalls. Below is a compact, idiomatic way to do it in a single expression while avoiding the usual snags.
Problem overview
The goal is to build a finite prefix of the iterative application of a function, starting from a seed. For a concrete example, consider doubling modulo 99. We want the first N elements of the chain starting at 1: 1, f(1), f(f(1)), … where f(v) = (2 * v) % 99.
Baseline attempts that look fine but have drawbacks
A straightforward generator does the job, but it evaluates the function once more than needed, producing a value that is never yielded. That extra call is avoidable.
def run_chain(seed, step, count=10):
for _ in range(count):
yield seed
seed = step(seed)
run_chain(1, lambda t: (2 * t) % 99)
Another idea is to co-opt itertools.accumulate with a binary lambda that ignores its second argument. It works, but it’s an awkward fit because accumulate is designed for binary aggregation over an input iterable.
from itertools import accumulate
list(accumulate([None] * 10, lambda a, b: 2 * a, initial=1))
In this pattern the second parameter is dead weight. If you really go this route, making the intent explicit with an unused placeholder makes it clearer: lambda a, _: 2 * a.
The one-liner that does exactly what’s needed
A list comprehension with an assignment expression lets you initialize the sequence once and then repeatedly apply the function without extra work. The counter decides whether to set the seed or apply the step function.
h = lambda u: (2 * u) % 99
[s := h(s) if k else 1 for k in range(10)]
This yields the desired sequence:
[1, 2, 4, 8, 16, 32, 64, 29, 58, 17]
If you want to avoid hard-coding the initial value and make the seed explicit, keep the same idea and switch the first element to the provided seed:
h = lambda u: (2 * u) % 99
start = 1
[s := h(s) if idx else start for idx in range(10)]
Why the earlier options fall short
The generator pattern computes one additional step that goes unused: after yielding the last element, it still applies step once more before the loop ends. The accumulate workaround forces a binary function signature and discards a parameter purely to cooperate with the API, which obscures intent. The assignment-expression comprehension sidesteps both issues by seeding exactly once and then chaining applications of the function.
Practical notes that keep code clean
Be careful with naming. Using iter as a function name is a footgun because iter is already a built-in. A clearer, conflict-free name such as iterate_function, run_chain, or any descriptive alternative avoids shadowing and confusion. And if you ever do reach for accumulate in this context, make the ignored parameter explicit with an underscore to signal intent.
Takeaway
When you need a compact, single-expression sequence of x, f(x), f(f(x)), …, a list comprehension with an assignment expression is both precise and readable. It avoids the unused final call of a naïve generator and the unnatural contortions of accumulate, while keeping the initialization visible and intentional.