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
result_dot = flatten_dict(nested, sep=".")
print(result_dot)
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}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.
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})}
result = flatten_dict(nested)
print(result)Output:
{'a_b': 1, 'a_c': 2}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 |
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.