Problem Solved

Python · flatten a list

Home › Courses › Python › Problems › Flatten a list
🐍 Concept · 3 min read

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.

flatten.py
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.

deep.py
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.

special.py
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.

1. sum(nested, []) for large data

O(n²). Fix: chain.from_iterable or comprehension.

2. chain.from_iterable on strings

Yields characters. Fix: ''.join(words) or append.

3. chain(*nested) for large lists

Unpacks into one call — fails for huge lists. Fix: chain.from_iterable(nested).

4. Expecting chain to flatten multiple levels

It's one level. Fix: recursion with yield from.

5. isinstance(item, list) for tuples

Tuples stay nested. Fix: isinstance(item, (list, tuple)).

6. Forgetting that strings are iterable

Nested check for lists only — strings stay flat. Fix: check with isinstance(item, list).

7. Very deep nesting causes RecursionError

Python's limit is 1000. Fix: iterative version with a stack.

8. Using NumPy for non-numeric data

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

🗺️ Python learning path
The full roadmap from install to first project — every concept in order

Next in this series:

🧠 Metaclasses in Python
The classes of classes — and why you probably don't need them
← All Python problems
Problem solved · Python