Skip to main content

How to Flatten a Nested Dictionary in Python

A nested dictionary is a dictionary that contains other dictionaries as values, potentially multiple levels deep. Flattening a nested dictionary means converting it into a single-level dictionary where the keys represent the full path to each value, typically joined by a separator like an underscore (_) or a dot (.).

This operation is commonly needed when preparing data for CSV export, database insertion, logging, API payloads, or any system that requires flat key-value structures.

In this guide, you will learn multiple methods to flatten nested dictionaries in Python - from recursive approaches to iterative solutions using stacks and queues - with clear examples, customizable separators, and edge case handling.

Understanding the Problem

Given a nested dictionary:

nested = {
"a": 1,
"b": {"x": 2, "y": {"z": 3}},
"c": {"m": 4},
}

Flatten it into:

{"a": 1, "b_x": 2, "b_y_z": 3, "c_m": 4}

Each nested key is concatenated with its parent keys using a separator (_), forming a unique path to the value.

The most intuitive and widely-used approach uses recursion to traverse the nested structure:

def flatten_dict(d, parent_key="", sep="_"):
"""Recursively flatten a nested dictionary."""
items = {}
for key, value in d.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
items.update(flatten_dict(value, new_key, sep))
else:
items[new_key] = value
return items


nested = {
"a": 1,
"b": {"x": 2, "y": {"z": 3}},
"c": {"m": 4},
}

result = flatten_dict(nested)
print(result)

Output:

{'a': 1, 'b_x': 2, 'b_y_z': 3, 'c_m': 4}

How it works:

  1. For each key-value pair, a new key is constructed by joining the parent key with the current key.
  2. If the value is a dictionary, the function calls itself recursively with the updated parent key.
  3. If the value is not a dictionary, it's added to the result with the full concatenated key.

Using a Custom Separator

# Using dot notation (common for config files and APIs)
result_dot = flatten_dict(nested, sep=".")
print(result_dot)

# Using forward slash (path-like)
result_slash = flatten_dict(nested, sep="/")
print(result_slash)

Output:

{'a': 1, 'b.x': 2, 'b.y.z': 3, 'c.m': 4}
{'a': 1, 'b/x': 2, 'b/y/z': 3, 'c/m': 4}
tip

The recursive approach is the most readable and flexible solution. The configurable sep parameter makes it adaptable to different naming conventions (underscores for databases, dots for JSON paths, slashes for file-like hierarchies).

Method 2: Using a Stack (Iterative)

For very deeply nested dictionaries where recursion depth might be a concern, use an iterative approach with a stack:

def flatten_dict_stack(d, sep="_"):
"""Flatten a nested dictionary using a stack (iterative, DFS)."""
result = {}
stack = [(d, "")]

while stack:
current, parent_key = stack.pop()
for key, value in current.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
stack.append((value, new_key))
else:
result[new_key] = value

return result


nested = {
"a": 1,
"b": {"x": 2, "y": {"z": 3}},
"c": {"m": 4},
}

result = flatten_dict_stack(nested)
print(result)

Output:

{'a': 1, 'c_m': 4, 'b_x': 2, 'b_y_z': 3}

How it works:

  1. A stack holds tuples of (current_dict, parent_key).
  2. In each iteration, a dictionary is popped from the stack.
  3. For each key-value pair, if the value is a dictionary, it's pushed back onto the stack for further processing.
  4. Non-dictionary values are added directly to the result.
info

The stack-based approach processes dictionaries in depth-first order (last-in, first-out). The output key order may differ from the recursive method, but the content is identical. Since Python 3.7+, dictionary insertion order is preserved, but the processing order depends on the traversal strategy.

Method 3: Using a Queue (Iterative, BFS)

A queue (using collections.deque) processes dictionaries in breadth-first order, which can be useful when you want to process top-level keys before deeper ones:

from collections import deque

def flatten_dict_queue(d, sep="_"):
"""Flatten a nested dictionary using a queue (iterative, BFS)."""
result = {}
queue = deque([(d, "")])

while queue:
current, parent_key = queue.popleft()
for key, value in current.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
queue.append((value, new_key))
else:
result[new_key] = value

return result


nested = {
"a": 1,
"b": {"x": 2, "y": {"z": 3}},
"c": {"m": 4},
}

result = flatten_dict_queue(nested)
print(result)

Output:

{'a': 1, 'b_x': 2, 'c_m': 4, 'b_y_z': 3}

Notice that b_x and c_m (level 2 keys) appear before b_y_z (level 3) because of the breadth-first processing order.

Handling Edge Cases

Empty Nested Dictionaries

def flatten_dict(d, parent_key="", sep="_"):
"""Recursively flatten a nested dictionary."""
items = {}
for key, value in d.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
items.update(flatten_dict(value, new_key, sep))
else:
items[new_key] = value
return items


nested = {"a": 1, "b": {}, "c": {"d": {}}}

result = flatten_dict(nested)
print(result)

Output:

{'a': 1}

Empty dictionaries produce no keys in the output. If you want to preserve them, add explicit handling:

def flatten_dict_keep_empty(d, parent_key="", sep="_"):
"""Flatten dict, preserving empty dicts as None values."""
items = {}
for key, value in d.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
if not value: # Empty dict
items[new_key] = None
else:
items.update(flatten_dict_keep_empty(value, new_key, sep))
else:
items[new_key] = value
return items

result = flatten_dict_keep_empty({"a": 1, "b": {}, "c": {"d": {}}})
print(result)

Output:

{'a': 1, 'b': None, 'c_d': None}

Lists Inside Nested Dictionaries

By default, lists are treated as leaf values. If you want to flatten lists with index-based keys:

def flatten_dict_with_lists(d, parent_key="", sep="_"):
"""Flatten dict, including lists with index-based keys."""
items = {}
for key, value in d.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
items.update(flatten_dict_with_lists(value, new_key, sep))
elif isinstance(value, list):
for i, item in enumerate(value):
list_key = f"{new_key}{sep}{i}"
if isinstance(item, dict):
items.update(flatten_dict_with_lists(item, list_key, sep))
else:
items[list_key] = item
else:
items[new_key] = value
return items


nested = {
"name": "Alice",
"scores": [95, 87, 92],
"address": {"city": "NYC", "zip": "10001"},
}

result = flatten_dict_with_lists(nested)
print(result)

Output:

{'name': 'Alice', 'scores_0': 95, 'scores_1': 87, 'scores_2': 92, 'address_city': 'NYC', 'address_zip': '10001'}

Common Mistake: Not Checking for Dictionary Type Properly

A subtle bug occurs when values are dictionary-like objects (e.g., OrderedDict, defaultdict) but fail an isinstance(value, dict) check in some edge cases:

Potential issue with custom mappings:

from collections import OrderedDict

def flatten_dict(d, parent_key="", sep="_"):
"""Recursively flatten a nested dictionary."""
items = {}
for key, value in d.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
items.update(flatten_dict(value, new_key, sep))
else:
items[new_key] = value
return items

nested = {"a": OrderedDict({"b": 1, "c": 2})}

# This works because OrderedDict is a subclass of dict
result = flatten_dict(nested)
print(result)

Output:

{'a_b': 1, 'a_c': 2}
note

For maximum compatibility, use collections.abc.Mapping:

from collections.abc import Mapping

def flatten_dict_safe(d, parent_key="", sep="_"):
"""Flatten any Mapping type, not just dict."""
items = {}
for key, value in d.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, Mapping):
items.update(flatten_dict_safe(value, new_key, sep))
else:
items[new_key] = value
return items
caution

Using isinstance(value, dict) works for standard dictionaries and their subclasses. If your code needs to handle arbitrary mapping types, use collections.abc.Mapping instead for broader compatibility.

Unflattening: Reverse Operation

If you need to convert a flat dictionary back to a nested one:

def unflatten_dict(d, sep="_"):
"""Convert a flat dictionary back to a nested dictionary."""
result = {}
for key, value in d.items():
parts = key.split(sep)
current = result
for part in parts[:-1]:
current = current.setdefault(part, {})
current[parts[-1]] = value
return result


flat = {"a": 1, "b_x": 2, "b_y_z": 3, "c_m": 4}

nested = unflatten_dict(flat)
print(nested)

Output:

{'a': 1, 'b': {'x': 2, 'y': {'z': 3}}, 'c': {'m': 4}}

Comparison of Methods

MethodTraversal OrderMax Depth LimitReadabilityBest For
RecursionDepth-firstLimited by Python recursion limit (~1000)✅ HighestGeneral use (recommended)
Stack (iterative)Depth-first✅ No limit⚠️ ModerateVery deeply nested data
Queue (iterative)Breadth-first✅ No limit⚠️ ModerateLevel-order processing
info

Python's default recursion limit is approximately 1000 levels. For most real-world data (JSON APIs, config files, database records), this is more than sufficient. If you're working with extremely deeply nested structures, use the iterative stack or queue approach.

Summary

Flattening nested dictionaries is a fundamental operation for data transformation in Python. Key takeaways:

  • Use recursion for the most readable and flexible solution - supports custom separators and is easy to extend for lists and empty dicts.
  • Use a stack (iterative DFS) for very deeply nested dictionaries that might exceed Python's recursion limit.
  • Use a queue (iterative BFS) when you need breadth-first processing order.
  • Handle edge cases like empty dictionaries and nested lists explicitly based on your requirements.
  • Use collections.abc.Mapping instead of dict for type checking when working with custom mapping types.
  • The unflatten_dict() function provides the reverse operation when you need to reconstruct the nested structure.