Generating permutations lazily
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Generating permutations lazily means producing one arrangement at a time instead of materializing the full result set in memory. That distinction matters because permutation counts grow factorially. Even moderate input sizes become impractical if you try to build the whole list up front.
Lazy generation is the right default when you want to iterate, filter, or stop early. It keeps memory use small and lets the caller consume only as many permutations as needed.
Why Eager Generation Breaks Down
For n distinct items there are n! permutations. That grows quickly:
- '
5! = 120' - '
8! = 40,320' - '
10! = 3,628,800'
If you eagerly build a list of all permutations, both memory and startup time increase sharply. In many real tasks, such as search or constraint solving, you only need the first few valid candidates. Lazy generation avoids paying for results you never inspect.
Python Already Has a Lazy Iterator
In Python, itertools.permutations already yields permutations lazily:
This does not create all 24 permutations at once. It produces them on demand as the loop asks for them.
That is often the whole answer. If the standard library already matches your needs, use it.
Writing a Custom Lazy Generator
Sometimes you need custom pruning logic, extra metadata, or a different order. In that case, write a generator with yield:
This is recursive and easy to understand. Each call fixes one leading element and lazily delegates the remainder to smaller subproblems.
Why Laziness Is Useful
The biggest advantage is early termination. You can stop as soon as you find a result that satisfies a condition:
An eager approach would generate every permutation before the search even began.
Laziness also works well in pipelines. You can chain filtering, mapping, and search steps without building huge intermediate collections.
Handling Duplicate Elements
If the input contains duplicates, a naive generator returns duplicate permutations. For example, [1, 1, 2] has repeated outputs unless you deduplicate during generation.
One simple approach is to track which values were used at the current recursion depth:
This keeps the generation lazy while avoiding repeated work for duplicate values.
Complexity Still Matters
Laziness does not change the total number of permutations. If you iterate through all of them, the work is still factorial. What changes is memory usage and the ability to stop early.
That distinction matters when explaining performance. A lazy permutation generator is not magically fast; it is incremental. That is a major improvement for many workloads, but it does not remove the combinatorial explosion.
Common Pitfalls
The most common mistake is immediately converting the lazy iterator back into a list with list(permutations(items)). That defeats the whole point and brings back the memory cost.
Another mistake is assuming lazy generation reduces time even when you consume every permutation. If you eventually iterate over the full set, laziness mostly helps memory, not asymptotic runtime.
Recursive generators also make duplicate handling easy to overlook. If the input can repeat values, add deduplication logic or you may get repeated permutations.
Finally, be mindful of recursion depth for larger inputs in custom implementations. The standard library version is usually more robust than a handwritten recursive generator.
Summary
- Lazy generation yields permutations one at a time instead of storing all of them.
- '
itertools.permutationsalready provides a lazy iterator in Python.' - Custom generators are useful when you need pruning, custom ordering, or duplicate handling.
- Laziness saves memory and enables early termination, but it does not remove factorial growth.
- Avoid converting a lazy iterator into a full list unless you truly need every permutation at once.

