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.
Method 1: Using Recursion (Recommended)
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:
- For each key-value pair, a new key is constructed by joining the parent key with the current key.
- If the value is a dictionary, the function calls itself recursively with the updated parent key.
- 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}
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:
- A stack holds tuples of
(current_dict, parent_key). - In each iteration, a dictionary is popped from the stack.
- For each key-value pair, if the value is a dictionary, it's pushed back onto the stack for further processing.
- Non-dictionary values are added directly to the result.
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}
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
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
| Method | Traversal Order | Max Depth Limit | Readability | Best For |
|---|---|---|---|---|
| Recursion | Depth-first | Limited by Python recursion limit (~1000) | ✅ Highest | General use (recommended) |
| Stack (iterative) | Depth-first | ✅ No limit | ⚠️ Moderate | Very deeply nested data |
| Queue (iterative) | Breadth-first | ✅ No limit | ⚠️ Moderate | Level-order processing |
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.Mappinginstead ofdictfor type checking when working with custom mapping types. - The
unflatten_dict()function provides the reverse operation when you need to reconstruct the nested structure.