How to Flatten a Nested List in Python
python flatten nested list: Learn how to flatten nested lists in Python using list comprehension, itertools.chain, recursion, and generators. Compare readability, perf...
When working with data in Python, you often encounter nested lists—lists that contain other lists as elements. The operation of converting such a structure into a single flat list is known as flattening. The right way to python flatten nested list depends on the depth of nesting, the types of elements, and whether you need to preserve order or handle arbitrarily deep structures. This article walks through the common approaches, their tradeoffs, and the situations where each is most appropriate.
The Basic Case: Flattening One Level of Nesting
If your list contains only one level of nesting—for example, [[1, 2], [3, 4]]—the simplest solution is a list comprehension that iterates over each sublist and then over each item inside it.
nested = [[1, 2], [3, 4]] flat = [item for sublist in nested for item in sublist] print(flat) # [1, 2, 3, 4]
The comprehension reads as: for each sublist in nested, for each item in sublist, collect item. This is concise and fast for a single level. It also works when some elements are not lists, as long as you only want to flatten the outermost list. For example, [1, [2, 3], 4] becomes [1, 2, 3, 4] because the non-list elements are taken as-is.
For the same one-level case, the itertools module provides chain.from_iterable, which is often slightly more readable when you already use itertools elsewhere.
from itertools import chain nested = [[1, 2], [3, 4]] flat = list(chain.from_iterable(nested)) print(flat) # [1, 2, 3, 4]
Both approaches produce the same result, but the list comprehension is more explicit about the iteration order. chain.from_iterable is marginally faster in CPython because it avoids the inner loop in Python bytecode, but the difference is rarely significant unless you are flattening millions of sublists.
Flattening Arbitrarily Deep Nested Lists
When the nesting depth is unknown or can vary, a one-level approach fails. For example, [1, [2, [3, [4]]]] requires a recursive solution. A recursive function can traverse the list and extend a result list whenever it encounters a non-list element.
def flatten_recursive(nested): result = [] for item in nested: if isinstance(item, list): result.extend(flatten_recursive(item)) else: result.append(item) return result print(flatten_recursive([1, [2, [3, [4]]]])) # [1, 2, 3, 4]
The function checks each element. If it is a list, it recurses and extends the result with the flattened sublist; otherwise, it appends the element directly. This works for any depth, but it has two limitations: it creates a new result list at each recursion level, and it is limited by Python's recursion depth (default 1000). For deeply nested structures, you may hit RecursionError.
A generator-based version avoids building intermediate lists and yields elements lazily, which is more memory-efficient for large inputs.
def flatten_generator(nested): for item in nested: if isinstance(item, list): yield from flatten_generator(item) else: yield item flat = list(flatten_generator([1, [2, [3, [4]]]])) print(flat) # [1, 2, 3, 4]
yield from delegates the iteration to the recursive call, so the generator only produces one element at a time. This is the preferred approach when you need to process a large flattened sequence without holding all values in memory at once.
Handling Mixed Types and Non-List Elements
The recursive examples above treat any list as a container to flatten, and any other object as a leaf. This works well when your data is homogeneous—all leaves are numbers or strings. However, if your list contains tuples, sets, or other iterables that you do not want to flatten, you must adjust the condition.
For instance, if you want to flatten only lists but keep tuples intact, change the check to isinstance(item, list) as shown. If you want to flatten any iterable except strings, you need a different check because strings are iterable but usually should be treated as atomic values.
def flatten_except_strings(nested): for item in nested: if isinstance(item, list) and not isinstance(item, str): yield from flatten_except_strings(item) else: yield item
This version treats strings as leaves, which is usually the desired behavior. Be aware that isinstance(item, list) will not flatten tuples. If you need to flatten tuples as well, use isinstance(item, (list, tuple)). The key is to define exactly what counts as a "container" for your specific data schema.
Performance and Memory Considerations
The choice between recursion and iteration affects both speed and memory usage. For a single level of nesting, list comprehension and itertools.chain are both efficient because they are implemented in C and avoid Python-level recursion. For deeper structures, the recursive generator is generally more memory-friendly than the recursive list-returning function, because it does not allocate a new list at each level. However, recursion still consumes stack frames, and the recursion depth limit can become a bottleneck for pathological inputs.
If you need to flatten a list that is thousands of levels deep, recursion will fail. In that case, an iterative approach using an explicit stack is safer, though more verbose.
def flatten_iterative(nested): stack = list(reversed(nested)) result = [] while stack: item = stack.pop() if isinstance(item, list): stack.extend(reversed(item)) else: result.append(item) return result
This uses a stack to avoid recursion entirely. It processes elements in the same order as the recursive version because we reverse the list before pushing onto the stack. The memory usage is proportional to the number of items on the stack at any time, which is typically much smaller than the total number of elements. This is the most robust option for arbitrarily deep structures, but it is less readable and should only be used when recursion depth is a real concern.
Choosing the Right Approach for Your Use Case
| Approach | Depth Limit | Memory Profile | Readability | Best For |
|---|---|---|---|---|
| List comprehension | One level | Creates full result list | High | Simple, known depth |
itertools.chain | One level | Creates full result list | Medium | One-level flattening in existing code |
| Recursive list builder | Recursion | Intermediate lists | Medium | Moderate depth, small data |
| Recursive generator | Recursion | Lazy, low peak memory | Medium | Large data, streaming processing |
| Iterative with stack | None | Stack proportional to depth | Low | Very deep nesting, production safety |
Use a one-level method when you know the structure is always [[...]] and no deeper nesting exists. Use the recursive generator when you need to handle arbitrary depth but the data size is manageable and recursion depth is not a concern. Use the iterative stack when you must guarantee no RecursionError and the input could be extremely deep. The generator is often the best balance of clarity and memory efficiency for most real-world data, such as JSON payloads that may nest a few levels but not hundreds.
Edge Cases and Common Pitfalls
Several edge cases can trip up a naive flattening implementation. First, empty lists: both recursive and iterative versions handle them correctly because they simply contribute no elements. Second, lists containing None or other falsy values: the isinstance check does not depend on truthiness, so None is treated as a leaf, which is usually correct. Third, cycles: if a list contains a reference to itself, any recursive or iterative flattening will loop indefinitely. There is no built-in cycle detection, so you must either guarantee acyclic input or add a seen-set that tracks object IDs, which complicates the implementation.
Another common mistake is using isinstance(item, list) when the data contains tuples or other sequence types that should be flattened. Conversely, using a broad check like isinstance(item, Iterable) will treat strings as containers, splitting them into characters. Always test your flattening function with a sample that includes the actual types you expect, including any unusual but valid elements like dictionaries or custom objects.
Finally, consider whether you need to preserve the original order. All the methods shown here preserve the left-to-right depth-first order, which is what most applications expect. If you need a different ordering, such as breadth-first, you would need to adjust the algorithm accordingly. Flattening is a fundamental operation, and choosing the right implementation now can save you from subtle bugs later when your data grows or changes shape.