Flattening Nested Lists in Python: Every Method, Every Trade-off

A nested list is exactly what it sounds like: a list that contains other lists as elements. Flattening it means collapsing that multi-level structure into a single, one-dimensional sequence. Python gives you more than half a dozen ways to do this, and choosing the wrong one can make code brittle, slow, or silently incorrect. This guide covers every major approach with precise explanations and honest trade-offs.

Whether you are cleaning data from an API, building a pipeline that receives inconsistently shaped inputs, or processing a matrix from a numerical computation, flattening nested lists is a task you will encounter repeatedly. What makes it interesting is that Python has no built-in flatten() function in the standard library. That absence is not an oversight. As a 2023 thread on Python Discuss notes, there are two widely accepted Pythonic forms for one-level flattening, and the community has never converged on a single canonical answer for arbitrary-depth cases. Understanding why that is will make you a better programmer, not just a faster one.

What a Nested List Actually Is

In Python, a list is an ordered, mutable sequence of references to objects. When one of those objects happens to be another list, you have nesting. There is no formal limit on depth. A list can be nested to any depth you have memory for:

# Depth 1: a flat list
flat = [1, 2, 3, 4, 5]

# Depth 2: a list of lists (the most common case)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Depth 3: irregularly nested
jagged = [[1, 2, [3, 4]], [5, [6, [7]]]]

# Mixed types within nesting
mixed = [[1, "a"], [True, 3.14], [[None, 2]]]

The depth of a nested list is the number of levels of nesting. A flat list has depth 1. The matrix above has depth 2. jagged has depth 3. When people talk about "flattening," they usually mean one of two distinct operations:

  • Shallow flattening removes exactly one level of nesting. A depth-2 list becomes depth-1. A depth-3 list becomes depth-2.
  • Deep flattening removes all levels of nesting, regardless of how deeply items are nested.

This distinction matters enormously when choosing a method. Applying a shallow method to a deeply nested structure does not raise an error by default — it silently produces incorrect results by leaving inner lists intact in the output.

Note

Python's official documentation does not define "shallow" vs. "deep" flattening as standard terminology. These are community-adopted terms, not language-level concepts. Keep that in mind when discussing the topic with others who may use different language.

Shallow Flattening: One Level at a Time

The For Loop with .extend()

The most explicit and beginner-readable approach is an imperative loop. You create an empty result list, iterate over the outer list, and extend the result with each sublist:

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

flat = []
for sublist in nested:
    flat.extend(sublist)

print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Using .extend() is meaningfully different from using .append() inside a nested loop. .extend() adds each element of the sublist individually, while .append() would add the whole sublist as a single item. This approach has O(n) time complexity where n is the total number of elements, and it is predictable in memory behavior.

You can also use the augmented assignment operator += in place of .extend(), and Real Python's benchmark testing has found that a for loop with .extend() or += is among the fastest options for shallow flattening:

flat = []
for sublist in nested:
    flat += sublist

print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

List Comprehension

List comprehension offers a more concise syntax. The double-loop form reads as: "for each sublist in nested, for each item in sublist, give me item":

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

flat = [item for sublist in nested for item in sublist]

print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

The order of the for clauses inside a list comprehension mirrors the order they would appear in an equivalent nested for loop, reading left to right. This trips up beginners who instinctively reverse the order. Once you internalize it, this is widely considered the most Pythonic one-liner for depth-2 lists.

Pro Tip

If you can only memorize one rule: the for-clause order inside a list comprehension mirrors the order you would write the loops outside. [x for outer in big_list for x in outer] is the same as writing for outer in big_list: for x in outer:.

itertools.chain.from_iterable()

Python's standard library itertools module provides chain.from_iterable(), which is specifically designed for this use case. It takes a single iterable of iterables and chains them together lazily:

import itertools

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

flat = list(itertools.chain.from_iterable(nested))

print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

There is an older idiom using itertools.chain(*nested), which unpacks the outer list with the splat operator. The from_iterable() form is preferred because unpacking with * materializes the entire outer list into positional arguments before calling the function, which wastes memory on large datasets. from_iterable() consumes the outer iterable lazily.

Because chain.from_iterable() returns an iterator rather than a list, wrapping it in list() forces evaluation. If you are feeding the result into another iterator-consuming function such as sum() or set(), you can skip the list() call entirely and save an allocation.

The sum() Trick and Why You Should Avoid It

A commonly shared one-liner uses Python's built-in sum() with an empty list as the initial value:

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

flat = sum(nested, [])

print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

This works because list concatenation is valid in Python and sum() starts from the provided initial value, concatenating each sublist in turn. It is clever and compact, but it has a serious flaw: quadratic time complexity. Each concatenation step creates a new list object. With 10,000 sublists, this approach runs roughly 100 times slower than itertools.chain, as confirmed in published benchmarks from Kanaries. It should be reserved for throwaway scripts on tiny datasets where readability temporarily outweighs correctness of choice.

"The sum() and basic reduce approaches are an order of magnitude slower due to quadratic list copying. Picking the wrong method can cost you." — Kanaries Python Flatten List benchmarks, 2025

functools.reduce()

The functional programming approach uses functools.reduce() to apply list concatenation cumulatively:

import functools
import operator

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

# Using operator.concat (equivalent to +)
flat = functools.reduce(operator.concat, nested)

# Using operator.iadd (in-place add) for better performance
flat = functools.reduce(operator.iadd, nested, [])

print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

The first form using operator.concat has the same quadratic problem as sum(). The second form using operator.iadd performs in-place addition, which mutates the accumulator list rather than creating a new one at each step. This recovers much of the lost performance and makes reduce competitive again. That said, neither form is as readable or straightforward as itertools.chain, so unless you are working in a functional programming context where reduce is idiomatic to your codebase, it is not the first tool to reach for.

Deep Flattening: Arbitrary Nesting

When your data can be nested to unknown or variable depth, shallow methods fail silently. You need a strategy that recurses through the entire structure. Python provides several, each with important caveats.

Recursive Flattening with isinstance()

The standard recursive approach checks whether each element is itself a list. If it is, the function calls itself on that element and extends the result. If it is not, it appends the element directly:

def flatten_recursive(lst):
    flat = []
    for item in lst:
        if isinstance(item, list):
            flat.extend(flatten_recursive(item))
        else:
            flat.append(item)
    return flat

jagged = [[1, 2, [3, 4]], [5, [6, [7]]]]
print(flatten_recursive(jagged))
# [1, 2, 3, 4, 5, 6, 7]

This is clean and correct for typical use cases. The recursion depth mirrors the nesting depth of the data, not the number of elements. A list with 1,000,000 elements but only 5 levels of nesting will recurse exactly 5 times per branch, which is perfectly safe.

Warning

Python's default recursion limit is typically 1,000 frames (verifiable via sys.getrecursionlimit()). A list nested 1,000+ levels deep will raise a RecursionError. If your data can be pathologically deep, use an iterative stack-based approach instead of raw recursion.

Python's Recursion Limit in Depth

CPython sets a default stack depth of 1,000 to prevent C-level stack overflows. As the Python documentation for sys.setrecursionlimit() explains, this value limits the maximum depth of the Python interpreter stack, not simply the number of recursive calls. You can raise this value:

import sys
sys.setrecursionlimit(5000)  # Increase with caution

Doing so comes with a genuine risk. Python does not perform tail-call optimization. Languages like Lisp and C can optimize tail-recursive calls so that only one stack frame is active at a time, making deep recursion safe. Python cannot. Raising the recursion limit trades the predictable RecursionError for the possibility of a hard segmentation fault, depending on the platform. The correct long-term fix for deeply nested data is an iterative algorithm.

Iterative Deep Flattening with an Explicit Stack

You can replicate recursive behavior without using the call stack by maintaining an explicit stack data structure. This approach handles any nesting depth without risk of hitting the interpreter limit:

def flatten_iterative(lst):
    flat = []
    stack = list(lst)  # copy to avoid mutating the original
    while stack:
        item = stack.pop()
        if isinstance(item, list):
            stack.extend(item)  # push inner items back on the stack
        else:
            flat.append(item)
    flat.reverse()  # pop() takes from the end; reverse restores order
    return flat

jagged = [[1, 2, [3, 4]], [5, [6, [7]]]]
print(flatten_iterative(jagged))
# [1, 2, 3, 4, 5, 6, 7]

Because stack.pop() pulls from the right side of the list (last-in, first-out), the elements emerge in reverse order. The final flat.reverse() call restores original order. This is O(n) time and O(n) space, where n is the total number of elements across all levels of nesting. It is the production-appropriate replacement for recursion when nesting depth is unknown or could be large.

Generator-Based Approaches

When the flattened output will be consumed once rather than stored, a generator is more memory-efficient than building a full list. A generator yields elements one at a time, which is ideal for large datasets processed in pipelines:

def flatten_gen(lst):
    for item in lst:
        if isinstance(item, list):
            yield from flatten_gen(item)
        else:
            yield item

jagged = [[1, 2, [3, 4]], [5, [6, [7]]]]

# Consuming the generator
for value in flatten_gen(jagged):
    print(value, end=" ")
# 1 2 3 4 5 6 7

# Converting to list when needed
flat = list(flatten_gen(jagged))
print(flat)
# [1, 2, 3, 4, 5, 6, 7]

The yield from syntax, introduced in Python 3.3 (PEP 380), delegates to a sub-generator cleanly. Without it, you would need an inner for loop to re-yield each element from the recursive call, which is more verbose and slightly slower. Note that this generator approach is still recursive and subject to the same recursion depth limit. For a generator-based approach that avoids recursion entirely, you would need to implement a stack-based generator.

Third-Party Options: more-itertools

The more-itertools library extends Python's standard itertools with additional utilities. Its collapse() function handles deep flattening with sophisticated edge-case handling that is difficult to replicate manually:

# pip install more-itertools
from more_itertools import collapse

data = [[1, 2, [3]], (4, 5), "hello", [[6, [7]]]]
flat = list(collapse(data))
print(flat)
# [1, 2, 3, 4, 5, 'hello', 6, 7]

Two things stand out here. First, collapse() handles mixed iterables: tuples, lists, generators, and other sequences are all treated as containers to flatten, not as leaf values. Second, it treats strings as atomic values by default, which is the behavior you almost always want. A naive recursive implementation that checks isinstance(item, Iterable) would try to iterate into strings, producing an infinite loop because each character of a string is itself a one-character string.

The levels parameter gives you precise depth control:

from more_itertools import collapse

data = [[1, [2, [3, [4]]]], [5]]

print(list(collapse(data, levels=1)))
# [1, [2, [3, [4]]], 5]    -- one level removed

print(list(collapse(data, levels=2)))
# [1, 2, [3, [4]], 5]      -- two levels removed

print(list(collapse(data)))
# [1, 2, 3, 4, 5]          -- all levels removed

This combination of safety, flexibility, and correct string handling makes collapse() the most robust off-the-shelf solution when you already have more-itertools as a dependency, or when your data involves mixed iterable types.

NumPy for Numeric Data

If your nested list contains only numeric data and you are already working in a scientific computing context, NumPy provides its own flattening tools that operate on arrays:

import numpy as np

# Works when all sublists are the same length (regular shape)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = np.array(matrix).flatten()
print(flat)
# array([1, 2, 3, 4, 5, 6, 7, 8, 9])

# Convert back to Python list if needed
print(flat.tolist())
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# For irregular shapes, use numpy.concatenate
jagged = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = np.concatenate(jagged).tolist()
print(flat)
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

Important limitations apply. np.array(matrix).flatten() only works when all sublists have the same length. If you pass a jagged list to np.array(), NumPy will either raise a ValueError or produce an array of objects rather than numbers, depending on the version and the exact shape of the data. np.concatenate() handles the one-level jagged case correctly. For multi-level nesting with numeric data, the recursive or generator approaches are still necessary before NumPy enters the picture.

NumPy's real advantage is speed on large, regular data. It executes operations at the C level, making it substantially faster than pure Python for large numeric arrays that are already in memory.

Edge Cases That Will Surprise You

Strings Are Iterable

Strings in Python are sequences of characters, which means any flattening function that checks for generic iterability will attempt to iterate into them. A list like ["hello", [1, 2]] will produce ['h', 'e', 'l', 'l', 'o', 1, 2] if you use a generic iterable check rather than a list-specific check. The fix is to use isinstance(item, list) or isinstance(item, (list, tuple)) rather than hasattr(item, '__iter__').

# Dangerous: iterates into strings
from collections.abc import Iterable

def flatten_bad(lst):
    for item in lst:
        if isinstance(item, Iterable):
            yield from flatten_bad(item)
        else:
            yield item

data = ["hello", [1, 2]]
print(list(flatten_bad(data)))
# ['h', 'e', 'l', 'l', 'o', 1, 2]  -- probably not what you wanted

# Correct: exclude strings explicitly
def flatten_safe(lst):
    for item in lst:
        if isinstance(item, (list, tuple)) and not isinstance(item, str):
            yield from flatten_safe(item)
        else:
            yield item

print(list(flatten_safe(data)))
# ['hello', 1, 2]  -- strings treated as atomic values

Mixed Types and Non-List Containers

Real-world data often mixes lists with tuples, sets, or generators. A function that only checks isinstance(item, list) will not recurse into tuples. Whether that is correct behavior depends on your use case. If you are flattening data from an API that might return either lists or tuples at any level, you need to decide explicitly which container types should be treated as structural and which should be treated as leaf values.

def flatten_containers(lst, types=(list, tuple)):
    """Flatten any combination of the specified container types."""
    flat = []
    for item in lst:
        if isinstance(item, types):
            flat.extend(flatten_containers(item, types))
        else:
            flat.append(item)
    return flat

data = [[1, (2, 3)], [4, [5, (6, 7)]]]
print(flatten_containers(data))
# [1, 2, 3, 4, 5, 6, 7]

Empty Lists and Single-Element Lists

All the methods above handle empty sublists correctly: an empty sublist contributes nothing to the output, and the overall function returns an empty list if all sublists are empty. There is no special case needed. Single-element sublists work identically to multi-element ones. Where bugs tend to appear is when the outer structure itself is not a list, such as when a function receives a plain integer or string instead of a list, and the first call to iterate over it fails. Defensive input validation is worth adding in production code.

Performance Comparison

The performance of each method varies significantly with data size and nesting depth. The following table summarizes the relative performance characteristics for shallow (depth-2) and deep (depth-3+) cases based on published benchmark data:

Method Depth Handled Time Complexity Memory Verdict
itertools.chain.from_iterable() Shallow (1 level) O(n) Low (lazy) Best
List comprehension double-loop Shallow (1 level) O(n) Medium Excellent
For loop + .extend() Shallow (1 level) O(n) Medium Excellent
functools.reduce(operator.iadd) Shallow (1 level) O(n) Medium Good
sum(nested, []) Shallow (1 level) O(n²) High Avoid at Scale
Recursive with isinstance() Deep (any) O(n) Stack depth Good
Iterative stack Deep (any) O(n) O(n) Best (safe)
Generator (yield from) Deep (any) O(n) Very low Best (streams)
more_itertools.collapse() Deep (any) O(n) Low (lazy) Best (mixed types)
NumPy .flatten() Shallow (regular shape) O(n) Low (C) Best for numerics only

As Programmer Notes summarizes in a 2025 guide: for large datasets, itertools.chain.from_iterable() is typically the fastest and most memory-efficient method for shallow flattening. The lazy evaluation means the iterator does not build the full output list until you consume it, which matters when the data is large and you only need to iterate once.

Decision Framework

Choosing the right method comes down to answering four questions in order:

1. Is the nesting depth guaranteed to be exactly one level? If yes, use itertools.chain.from_iterable() for performance or list comprehension for readability. Both are fast, correct, and Pythonic.

2. Is the nesting depth unknown or variable? If yes, use the recursive isinstance() approach for typical data, or the iterative stack approach when nesting could be pathologically deep (more than a few hundred levels).

3. Will the result be consumed once in a pipeline? If yes, use a generator-based approach to avoid allocating a full output list. Pass the generator directly to whatever consumes it.

4. Does the data contain mixed container types (lists, tuples, generators) or are strings present? If yes, use more_itertools.collapse(), which handles these edge cases correctly and is well-tested against them. If you cannot add a third-party dependency, use the explicit isinstance(item, (list, tuple)) guard in your own recursive function.

Pro Tip

In production code, always document your assumption about nesting depth. A future maintainer reading list(chain.from_iterable(data)) should know that this call assumes data is exactly one level deep. A comment or a type annotation (e.g., data: list[list[int]]) makes that contract explicit.

Key Takeaways

  1. Shallow vs. deep is not optional: Applying a shallow method to deeply nested data does not raise an error; it produces silently wrong output. Always confirm your data's nesting structure before choosing a method.
  2. itertools.chain.from_iterable() wins for shallow flattening: It is lazy, O(n), part of the standard library, and avoids the memory overhead of unpacking with *.
  3. sum(nested, []) is a trap: It looks elegant but degrades to O(n²) due to repeated list copying. Avoid it on any dataset larger than a few hundred elements.
  4. Recursion depth is a real risk: Python's default stack limit of 1,000 frames means recursive flattening fails on data nested more than ~995 levels deep. Use an iterative stack-based approach when depth is unknown or potentially large.
  5. Strings require explicit handling: Any generic iterable check will recurse into strings. Use isinstance(item, list) or explicitly exclude strings to preserve them as atomic values.
  6. more-itertools.collapse() handles the hard cases: Mixed container types, depth control, and correct string behavior are all managed correctly out of the box, which is worth a dependency when your data is irregular.
  7. Generators save memory for one-pass consumption: When flattened data feeds into a stream, a generator avoids materializing the entire output list and can significantly reduce peak memory usage.

Flattening nested lists is one of those problems that looks trivial until it is not. The methods above cover every realistic case you will encounter in production Python. The key is matching the tool to the actual shape and size of your data rather than defaulting to whatever is shortest to type. Write the version that matches your requirements, document your depth assumption, and move on confidently.

Sources
  1. Kanaries. "Python Flatten List: 8 Methods to Flatten Nested Lists." docs.kanaries.net, 2025.
  2. Real Python. "How to Flatten a List of Lists in Python." realpython.com, updated December 2024.
  3. GeeksforGeeks. "Flatten a List of Lists in Python." geeksforgeeks.org, updated July 2025.
  4. Python Discuss. "Add flatten_list to itertools." discuss.python.org, 2023.
  5. Programmer Notes. "Python Flatten List of Lists: Simple Tricks for Fast Results." prognotes.net, December 2025.
  6. Python Central. "How to Flatten a List of Lists in Python." pythoncentral.io, June 2025.
  7. Python Central. "Resetting the Recursion Limit." pythoncentral.io.
  8. Python PEPs. "PEP 651 – Robust Stack Overflow Handling." peps.python.org.
  9. note.nkmk.me. "Get/Set the Recursion Limit in Python: sys.getrecursionlimit, sys.setrecursionlimit." note.nkmk.me, January 2024.
  10. C# Corner. "How to Convert a Nested List into a Flat List in Python." c-sharpcorner.com, October 2025.
back to articles