Skip to content

How to Flatten a Nested Dictionary in Python

Learn how to convert a multi-level Python dictionary into a single-level structure using recursion, stacks, or queues with customizable separators and edge case handling.

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

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:

  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.

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.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.
Navigation