How to flatten a list of lists.
chain, comprehension, recursion.
You have [[1, 2], [3, 4], [5]] and want [1, 2, 3, 4, 5]. Three methods, each appropriate in a different case. itertools.chain.from_iterable is the fastest. A list comprehension is the most readable. sum(nested, []) is the slowest and should be avoided for anything beyond tiny data. For deeply nested lists, use recursion. This page covers each method, the benchmarks, and the traps.
Short answer: For one level of nesting: list(itertools.chain.from_iterable(nested)) (fastest) or [x for sublist in nested for x in sublist] (readable). Never use sum(nested, []) — it's O(n²). For arbitrary depth: recursion with yield from. For strings: be careful — iterating a string gives characters, so use chain.from_iterable on the outer list, not on the strings themselves. For numeric arrays in a NumPy project: arr.ravel().
✅ The three methods
One level of nesting, three ways.
from itertools import chain
nested = [[1, 2], [3, 4], [5]]
# Method 1: list comprehension
flat1 = [x for sublist in nested for x in sublist]
print(flat1) # [1, 2, 3, 4, 5]
# Method 2: itertools.chain.from_iterable
flat2 = list(chain.from_iterable(nested))
print(flat2) # [1, 2, 3, 4, 5]
# Method 3: sum — works, but slow
flat3 = sum(nested, [])
print(flat3) # [1, 2, 3, 4, 5]
# All three produce the same result.
# The difference is performance and readability.
# chain is lazy — you can iterate without building a list
for x in chain.from_iterable(nested):
print(x) # 1 2 3 4 5, one at a time
# Comprehension is eager — builds the whole list
for x in [x for sub in nested for x in sub]:
print(x) # same, but list is built first
# For most code, comprehension is fine.
# For large data or memory-constrained contexts,
# chain.from_iterable is better.
# Quick benchmark on 1 million items:
import time
big = [[i] for i in range(1_000_000)]
t = time.perf_counter()
_ = list(chain.from_iterable(big))
print(f'chain: {time.perf_counter() - t:.3f}s') # ~0.100s
t = time.perf_counter()
_ = [x for sub in big for x in sub]
print(f'comprehension: {time.perf_counter() - t:.3f}s') # ~0.150s
t = time.perf_counter()
_ = sum(big, [])
print(f'sum: {time.perf_counter() - t:.3f}s') # ~5.0s
The three methods compared:
chain.from_iterable(nested):
✅ Fastest (implemented in C)
✅ Lazy — yields items one at a time
✅ O(n) memory for the list conversion,
O(1) if you iterate directly
✅ Works with any iterable-of-iterables
❌ Requires an import
❌ Only flattens one level
List comprehension:
✅ Readable
✅ No import needed
✅ Fast enough for most code
✅ Can add conditions inline
❌ Builds the whole list (eager)
❌ Only flattens one level
sum(nested, []):
✅ Shortest syntax
❌ O(n²) — unusable for large data
❌ Creates a new list at each step
❌ Only works with lists (not tuples/strings)
❌ Never the right answer
When to use each:
✅ chain.from_iterable — when you need speed
or are working with large/lazy data:
from itertools import chain
for item in chain.from_iterable(nested):
process(item) # memory-efficient
Or:
flat = list(chain.from_iterable(nested))
# fastest way to get a flat list
✅ Comprehension — for readability, small data,
or when you need conditions:
flat = [x for sub in nested for x in sub if x > 0]
# filter while flattening
flat = [f(x) for sub in nested for x in sub]
# transform while flattening
❌ sum(nested, []) — never for production code.
Use chain or comprehension instead.
The common one-liner versions:
# Shortest:
sum(nested, []) # slow
# Fastest:
list(chain.from_iterable(nested))
# Readable:
[x for sub in nested for x in sub]
The chain variants:
chain(nested[0], nested[1], ...) # multiple args
chain.from_iterable(nested) # one iterable
chain(*nested) # unpack — same as above
For flattening, from_iterable is the right one:
it takes the outer list and iterates the inner ones.
chain(*nested) also works but has a size limit —
the splat operator unpacks everything into one
call, which fails for huge lists (>~100k items).
from_iterable doesn't unpack — it iterates.
So:
❌ chain(*nested) for large nested
✅ chain.from_iterable(nested) always
⚠️ For a list of strings:
chain.from_iterable(['ab', 'cd']) gives 'a', 'b', 'c', 'd'
because chain iterates each string.
If you want ['ab', 'cd'] as flat, it's already flat.
If you want each character as an item:
[char for s in strings for char in s]
If you want them concatenated:
''.join(strings) # 'abcd'
✅ Arbitrary depth
Recursion for nested-in-nested.
from itertools import chain
# One level — works
nested = [[1, 2], [3, [4, 5]], [6]]
flat = list(chain.from_iterable(nested))
print(flat) # [1, 2, 3, [4, 5], 6]
# The [4, 5] is still nested.
# For arbitrary depth — recursion
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
deep = [[1, [2, [3, 4]]], 5, [[6]], [[[7]]]]
flat = list(flatten(deep))
print(flat) # [1, 2, 3, 4, 5, 6, 7]
# With mixed types (lists and tuples)
def flatten_mixed(nested):
for item in nested:
if isinstance(item, (list, tuple)):
yield from flatten_mixed(item)
else:
yield item
mixed = [1, (2, 3), [4, (5, 6)], 7]
print(list(flatten_mixed(mixed))) # [1, 2, 3, 4, 5, 6, 7]
# With a filter
def flatten_positive(nested):
for item in nested:
if isinstance(item, list):
yield from flatten_positive(item)
elif item > 0:
yield item
data = [[1, -2, [3, -4]], 5, -6]
print(list(flatten_positive(data))) # [1, 3, 5]
# For strings — treat them as atomic (don't iterate characters)
def flatten_no_strings(nested):
for item in nested:
if isinstance(item, list):
yield from flatten_no_strings(item)
else:
yield item # strings stay as one item
data = [['hello', 'world'], ['python']]
print(list(flatten_no_strings(data))) # ['hello', 'world', 'python']
# Not 'h', 'e', 'l', 'l', 'o'...
# For very deep nesting — iterative with a stack
def flatten_iter(nested):
stack = [iter(nested)]
while stack:
try:
item = next(stack[-1])
except StopIteration:
stack.pop()
continue
if isinstance(item, list):
stack.append(iter(item))
else:
yield item
# Same result, no recursion limit.
# Slower, but safe for depth > 1000.
Why recursion is necessary:
chain.from_iterable only iterates the OUTER list.
It yields each inner item as-is — including
nested lists.
[[1, 2], [3, [4, 5]]] → 1, 2, 3, [4, 5]
^ the [4, 5] is still a list
To go deeper, you need to detect nested lists
and recurse into them.
That's what the recursive flatten() does:
for each item:
if it's a list → recurse
else → yield the item
The yield from delegates to the recursive call,
flattening the result into the parent generator.
This handles arbitrary depth automatically.
The recursion limit:
Python's default recursion depth is 1000.
For lists nested deeper than 1000 levels,
flatten() raises RecursionError.
For most data, this is fine:
- JSON files rarely nest more than 10 levels
- Tree structures rarely exceed 50
- Deep nesting (>100) is usually a bug
For the rare case of very deep nesting,
use the iterative version with an explicit
stack. It's slower but avoids the recursion
limit.
Or increase the limit:
import sys
sys.setrecursionlimit(10000)
But this is risky — increasing the limit
can crash the interpreter if you exceed
the C stack.
The isinstance check:
isinstance(item, list) — item is a list?
For mixed types (lists and tuples):
isinstance(item, (list, tuple))
For any sequence except strings:
isinstance(item, (list, tuple, set)) or
hasattr(item, '__iter__') and not isinstance(item, str)
The last one is trickier:
- Sets are iterable but unordered
- Dicts are iterable (over keys)
- Bytes are iterable (over ints)
- Custom iterables are iterable
For most flatten problems, restrict to lists
(or lists + tuples). The general "flatten
anything iterable" case is rarely what you want.
The recursive pattern:
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item) # recurse
else:
yield item # base case
This is the template. Modify based on:
- What counts as "nested" (list, tuple, both)
- Whether to filter items (skip None, skip strings)
- Whether to transform items
The flatten-with-transform pattern:
def flatten_map(nested, f):
for item in nested:
if isinstance(item, list):
yield from flatten_map(item, f)
else:
yield f(item)
flat = list(flatten_map(data, lambda x: x * 2))
When to use recursion vs chain:
✅ chain.from_iterable — one level of nesting
→ [[1, 2], [3, 4]] — one level
→ Simpler, faster, no recursion limit
✅ Recursion — arbitrary depth
→ [1, [2, [3, 4]]] — multiple levels
→ Handles any nesting depth
✅ Iterative with stack — depth > 1000
→ Extremely deep nesting (rare)
→ Avoids RecursionError
How to tell:
If you know the structure is one level:
chain.from_iterable
If the structure could be any depth:
flatten() with recursion
If depth exceeds 1000:
iterative stack version
For 99% of real code, one level is enough,
and chain.from_iterable is the answer.
✅ Special cases
Strings, tuples, and NumPy.
from itertools import chain
# Strings — chain iterates characters
words = ['hello', 'world']
print(list(chain.from_iterable(words)))
# ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
# Usually not what you want!
# If you want the strings as items, they're already flat
# (a list of strings is flat — one level of nesting is 0)
# If you want to concatenate them:
print(''.join(words)) # 'helloworld'
print(' '.join(words)) # 'hello world'
# If you want each character as an item (rare):
print([c for word in words for c in word])
# ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
# Tuples — chain handles them
nested_tuples = [(1, 2), (3, 4), (5,)]
print(list(chain.from_iterable(nested_tuples)))
# [1, 2, 3, 4, 5]
# Mixed types
mixed = [[1, 2], (3, 4), [5]]
print(list(chain.from_iterable(mixed)))
# [1, 2, 3, 4, 5]
# chain iterates any iterable, including tuples.
# Strings with mixed types — be careful
mixed = [['hello'], [1, 2], ['world']]
print(list(chain.from_iterable(mixed)))
# ['hello', 1, 2, 'world']
# Each inner list contributes its items.
# The string 'hello' stays as one item.
# But if strings are the OUTER items:
words = ['hello', 'world']
print(list(chain.from_iterable(words)))
# ['h', 'e', 'l', 'l', 'o', ...]
# Because chain iterates each string.
# The rule:
# chain.from_iterable(xs) iterates each element of xs.
# If elements are strings, you get characters.
# If elements are lists/tuples, you get their items.
# If elements are scalars, you get a TypeError
# (can't iterate an int).
# NumPy — for numeric arrays
try:
import numpy as np
arr = np.array([[1, 2], [3, 4], [5, 6]])
print(arr.flatten()) # [1 2 3 4 5 6]
print(arr.ravel()) # [1 2 3 4 5 6] — view, faster
except ImportError:
print('NumPy not installed — skipping')
# The NumPy advantage:
# - Vectorized operations
# - ~10-100x faster than Python loops
# - Works for numeric data only (all dtypes must match)
#
# The NumPy disadvantage:
# - Heavy dependency
# - Requires equal-length sublists (2D array)
# - Converts everything to one dtype
# - Overkill for small data
# Dictionary flattening (a different problem)
d = {'a': 1, 'b': {'c': 2, 'd': 3}}
# There's no built-in for this. You need:
def flatten_dict(d, parent_key='', sep='.'):
items = []
for k, v in d.items():
new_key = f'{parent_key}{sep}{k}' if parent_key else k
if isinstance(v, dict):
items.extend(flatten_dict(v, new_key, sep).items())
else:
items.append((new_key, v))
return dict(items)
print(flatten_dict(d))
# {'a': 1, 'b.c': 2, 'b.d': 3}
# Different problem — dicts aren't sequences.
String trap explained:
['hello', 'world']
These are TWO strings. The list has 2 items.
But each string is iterable (over characters).
chain.from_iterable iterates the outer list
AND the inner strings:
outer → 'hello', 'world'
inner → 'h', 'e', 'l', 'l', 'o', 'w', ...
Result: 10 items, one per character.
When is this wanted:
→ Character-level processing
→ Text analysis
→ Rare
When it's a bug:
→ "I have a list of words, I want the same
list" — but you get characters
→ Fix: the list is already flat, don't flatten
For "list of sentences, want list of words":
sentences = ['Hello world', 'How are you']
words = [w for s in sentences for w in s.split()]
# ['Hello', 'world', 'How', 'are', 'you']
Not chain.from_iterable — because you're not
flattening the sentences, you're splitting them.
For "list of words, want characters":
chars = list(chain.from_iterable(words))
# chain is correct here
For "list of words, want joined string":
text = ''.join(words)
The pattern:
chain.from_iterable is for "iterable of iterables."
What's iterable:
→ List
→ Tuple
→ Set
→ String ← the trap
→ Dict (over keys)
→ Generator
→ File (over lines)
→ Range
chain.from_iterable(['ab', 'cd']) iterates
the strings, yielding characters.
If you want the strings as items, the list is
already flat — you don't need chain.
NumPy flattening:
arr = np.array([[1, 2], [3, 4]])
arr.flatten():
→ Returns a new 1D array
→ Always a copy
→ Works for any array (2D, 3D, ...)
→ Safe: modifying the result doesn't
affect the original
arr.ravel():
→ Returns a view (if possible)
→ Modifying the result MAY affect the
original
→ Faster (no copy)
→ Only for contiguous arrays
→ Use when you're reading, not writing
arr.reshape(-1):
→ Also returns a view
→ -1 means "figure out this dimension"
→ Equivalent to ravel() for most cases
For flattening:
✅ ravel() for read-only
✅ flatten() when you need a copy
✅ reshape(-1) — same as ravel
Performance:
For a 1000x1000 array:
ravel: ~1 μs (view)
flatten: ~1 ms (copy)
Python chain: ~100 ms
Comprehension: ~200 ms
NumPy is 100-1000x faster for numeric data.
But NumPy requires:
→ Equal-length sublists (2D array)
→ Same dtype for all items
→ A dependency
For a one-off flatten of a small list,
chain.from_iterable is fine.
For numeric data in a project already
using NumPy, ravel() is the right tool.
Dict flattening:
A dict with nested dicts is a different problem.
Dicts aren't sequences — they're mappings.
flatten_dict({'a': 1, 'b': {'c': 2}})
→ {'a': 1, 'b.c': 2}
The keys are concatenated with a separator.
This is used for:
→ Configuration merging
→ JSON path generation
→ Database column names
For a list-of-dicts, you'd use a comprehension
or chain over the values, but the "flatten"
concept is different.
Don't use sum(nested, []) for flattening. It works for tiny lists and is the shortest syntax, which makes it tempting. But it's O(n²) — for each element, it creates a new list by copying all previous elements. On 1 million items, it takes ~5 seconds versus ~0.1 seconds for chain.from_iterable. The performance cliff is sharp: 100 items is fine, 10,000 items is 100x slower than it should be, 1 million is unusable. The fix is one word: list(chain.from_iterable(nested)) or a comprehension. Both are O(n). Both are just as readable. Use them.
⚠️ Common mistakes
The flatten mistakes that cost real time.
sum(nested, []) for large dataO(n²). Fix: chain.from_iterable or comprehension.
chain.from_iterable on stringsYields characters. Fix: ''.join(words) or append.
chain(*nested) for large listsUnpacks into one call — fails for huge lists. Fix: chain.from_iterable(nested).
chain to flatten multiple levelsIt's one level. Fix: recursion with yield from.
isinstance(item, list) for tuplesTuples stay nested. Fix: isinstance(item, (list, tuple)).
Nested check for lists only — strings stay flat. Fix: check with isinstance(item, list).
Python's limit is 1000. Fix: iterative version with a stack.
Raises TypeError on mixed types. Fix: use Python for non-numeric.
❓ Frequently asked
How do I flatten a list of lists in Python?
Three main methods for one level. (1) List comprehension: [x for sublist in nested for x in sublist]. (2) itertools.chain.from_iterable(nested) — fastest, wrap in list(). (3) sum(nested, []) — concise but O(n²), avoid for large data. For one level, use chain.from_iterable for speed or the comprehension for readability. For arbitrary depth, use recursion.
What is the fastest way to flatten a list?
itertools.chain.from_iterable is the fastest for one level. Benchmarks on 1 million items: chain ~100ms, comprehension ~150ms, sum(nested, []) ~5s. Chain is faster because it's implemented in C and yields items lazily. For flat data with millions of items, use chain.from_iterable. Never use sum for flattening.
How do I flatten a deeply nested list?
For arbitrary depth, use recursion. Example: def flatten(nested): for item in nested: if isinstance(item, list): yield from flatten(item); else: yield item. This handles [[1, [2, [3, 4]]], 5] → [1, 2, 3, 4, 5]. For mixed types, adjust the isinstance check. For very deep nesting, use an explicit stack to avoid RecursionError.
Why is sum(nested, []) slow?
Because sum() calls list addition repeatedly. Each + creates a NEW list, copying all existing elements. For a list of n sublists, the total cost is O(n²). The chain.from_iterable approach is O(n). Don't use sum() except for tiny lists.
How do I flatten a list of strings?
Three options. (1) Join — ''.join(strings) concatenates into one string. (2) Split and join — for splitting words: ' '.join(words). (3) Treat as a list — if you want the strings as separate items, chain.from_iterable(strings) gives each string as one element. The trap: iterating a string gives characters.
Can I flatten a list with NumPy?
Yes — NumPy's flatten() and ravel() are very fast for numeric arrays. arr.flatten() returns a new 1D array; arr.ravel() returns a view if possible. For a list of equal-length sublists, np.array(nested).ravel() is the fastest option. Caveats: NumPy requires equal-length sublists, converts everything to one dtype, and is a heavy dependency.
🔗 Continue learning
Next in this series: