Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To select keys at every level of a nested dictionary, walk each key-value pair, recurse into nested mappings, and build a new result. The exact behavior depends on your contract: whether only dict values are traversed, whether ancestors of matches are retained, whether empty branches survive, and whether the input may be any mapping type.

For a practical dictionary-only filter that keeps selected keys and preserves the path to nested matches, use this recipe:

def select_keys(data, wanted):
    result = {}
    for key, value in data.items():
        if isinstance(value, dict):
            value = select_keys(value, wanted)
        if key in wanted:
            result[key] = value
        elif isinstance(value, dict) and value:
            result[key] = value
    return result

payload = {
    "id": 42,
    "profile": {
        "name": "Ada",
        "email": "ada@example.com",
        "preferences": {"theme": "dark", "language": "en"}
    },
    "orders": [{"id": 7, "total": 19.95}]
}

print(select_keys(payload, {"id", "email", "theme"}))

This returns {"id": 42, "profile": {"email": "ada@example.com", "preferences": {"theme": "dark"}}}. The orders list is not traversed because this function deliberately visits dictionaries only.

Decide what “recursively select keys” means

Python does not recursively filter dictionary values for you. A dictionary maps hashable keys to arbitrary objects, so a value might be another dictionary, a list, a custom object, or a self-reference. Recursion is therefore an explicit traversal policy, not automatic dictionary behavior. Python documents dict as its standard mapping type and Mapping as the interface for broader mapping implementations (built-in types; collections.abc).

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before writing the function, specify these choices:

  • Key rule: exact membership in a set, or a predicate such as a callable.
  • Traversal: nested dictionaries only, or dictionaries inside lists and tuples too.
  • Branch policy: keep a parent key because it matches, or keep an unselected ancestor when it contains a matching descendant.
  • Empty branches: retain {} or omit it.
  • Mutation: return a new object or edit the original.
  • Input type: built-in dictionaries only or any Mapping.

The examples below return new objects, retain ancestors of matches, omit empty unselected branches, and traverse mappings only unless stated otherwise.

Dictionary-only implementation

Keep matching keys and ancestors of nested matches

The function in the introduction recursively processes every dictionary value before deciding whether to retain the current pair. That order matters. If profile is not wanted but contains wanted keys, its filtered value is non-empty, so profile remains as the path to those keys.

def select_keys(data, wanted):
    """Return a new dict containing wanted keys and paths to nested matches."""
    result = {}
    for key, value in data.items():
        if isinstance(value, dict):
            value = select_keys(value, wanted)

        if key in wanted:
            # Matching values are retained, including an empty dict.
            result[key] = value
        elif isinstance(value, dict) and value:
            # Preserve an unselected parent only when descendants matched.
            result[key] = value
    return result

Matching keys retain their values as-is. Thus a selected key whose value is a list, string, number, or empty dictionary is not altered. Only dictionary values are recursively filtered.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep only selected keys, without ancestor branches

Sometimes the desired result is a flat selection at each visited level: an unselected parent should disappear even when a descendant matched. In that case, recurse only to inspect nested dictionaries and add a pair only when its own key matches.

def select_keys_no_ancestors(data, wanted):
    result = {}
    for key, value in data.items():
        if key in wanted:
            result[key] = value
        elif isinstance(value, dict):
            nested = select_keys_no_ancestors(value, wanted)
            # This branch is intentionally discarded because its parent key
            # did not match.
            if nested:
                pass
    return result

This version is useful only when you intentionally do not need the path to a nested result. In most JSON-like data, retaining ancestors is more useful because the output remains navigable.

Preserve or remove empty dictionaries

Empty-branch handling is a separate policy. The ancestor-preserving implementation removes an unselected parent after its recursive result becomes empty, but keeps an empty dictionary when the parent key itself is selected. To retain every visited dictionary, including empty unselected branches, use:

def select_keys_keep_empty(data, wanted):
    result = {}
    for key, value in data.items():
        if isinstance(value, dict):
            value = select_keys_keep_empty(value, wanted)
        if key in wanted or isinstance(value, dict):
            result[key] = value
    return result

Choose one policy and document it; callers often rely on the difference between a missing key and a present key whose value is {}.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a predicate when key names are not enough

A set gives exact, hash-based membership and works with non-string keys as long as they are hashable. For rules such as “all keys beginning with user_” or “integer keys greater than 10,” accept a callable:

from collections.abc import Callable
from typing import Any

def select_keys_where(data: dict, keep: Callable[[Any], bool]) -> dict:
    result = {}
    for key, value in data.items():
        if isinstance(value, dict):
            value = select_keys_where(value, keep)
        if keep(key):
            result[key] = value
        elif isinstance(value, dict) and value:
            result[key] = value
    return result

filtered = select_keys_where(
    {"user_id": 3, "meta": {"user_name": "Ada", "active": True}},
    lambda key: isinstance(key, str) and key.startswith("user_")
)

The predicate is called once for each visited key. If keys can have mixed types, test the type before applying string operations.

Accept any mapping with Mapping

isinstance(value, dict) includes dictionary subclasses (isinstance documentation), but excludes other mapping implementations. If your API should accept read-only mappings, ordered custom mappings, or proxy objects, use the abstract base class:

from collections.abc import Mapping, Collection
from typing import Any

def select_mappings(data: Mapping, wanted: Collection[Any]) -> dict:
    result = {}
    for key, value in data.items():
        if isinstance(value, Mapping):
            value = select_mappings(value, wanted)
        if key in wanted:
            result[key] = value
        elif isinstance(value, Mapping) and value:
            result[key] = value
    return result

This accepts any object implementing the mapping interface, including implementations recognized by collections.abc.Mapping. The output is always a built-in dict. If output type matters, define a reconstruction policy instead of assuming that calling type(data)(...) works: custom mappings may require constructor arguments or may be immutable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should lists and tuples be traversed?

Dictionary-only recursion is predictable and matches many JSON-shaped configuration objects, but nested records are often stored in arrays. Decide whether a list containing dictionaries is part of your traversal scope.

Traverse mappings inside sequences and preserve sequence types

from collections.abc import Mapping

def select_nested(value, wanted):
    if isinstance(value, Mapping):
        result = {}
        for key, child in value.items():
            filtered = select_nested(child, wanted)
            if key in wanted:
                result[key] = filtered
            elif isinstance(filtered, Mapping) and filtered:
                result[key] = filtered
        return result

    if isinstance(value, list):
        return [select_nested(item, wanted) for item in value]

    if isinstance(value, tuple):
        return tuple(select_nested(item, wanted) for item in value)

    return value

payload = {"users": [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Lin"}]}
print(select_nested(payload, {"id"}))
# {'users': [{'id': 1}, {'id': 2}]}

Here users remains because its list contains filtered records. Lists and tuples are rebuilt, while scalar values are returned unchanged. Sets require an additional policy because their members are unordered and may be unhashable after filtering.

Mutation, copying, and value ownership

All recipes above create new dictionaries, which avoids deleting keys while iterating and leaves the top-level input structure unchanged. They do not deep-copy arbitrary non-mapping values: a selected list, class instance, or other object is assigned to the result. Mutating that object later can therefore affect the original input. Use copy.deepcopy only when you explicitly need independent copies and understand the cost and behavior of custom objects.

A mutating implementation is possible, but it must iterate over a snapshot of keys and clearly document that callers lose data:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
def remove_unwanted_in_place(data, wanted):
    for key in list(data):
        value = data[key]
        if isinstance(value, dict):
            remove_unwanted_in_place(value, wanted)
        if key not in wanted and not (isinstance(value, dict) and value):
            del data[key]
    return data

Prefer the new-result approach for reusable libraries, validation pipelines, and code where the original payload may be needed for logging or retries.

Cycles and shared references

Typical decoded JSON is a tree and cannot contain cycles. Arbitrary Python objects can:

data = {}
data["self"] = data

Calling a simple recursive function on this value eventually raises RecursionError. If cycles are possible, either reject them as outside the function’s contract or track object identities. A common guard records id values currently being expanded:

from collections.abc import Mapping

def select_acyclic(data, wanted, active=None):
    if active is None:
        active = set()
    marker = id(data)
    if marker in active:
        raise ValueError("cyclic mapping is not supported")
    active.add(marker)
    try:
        result = {}
        for key, value in data.items():
            if isinstance(value, Mapping):
                value = select_acyclic(value, wanted, active)
            if key in wanted or (isinstance(value, Mapping) and value):
                result[key] = value
        return result
    finally:
        active.remove(marker)

The active-set approach detects a cycle on the current path while allowing the same mapping to be referenced by separate, non-cyclic branches. If preserving shared-reference identity is important, a memoization design is required instead of this simple tree-producing recipe.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Complexity and practical limits

For a tree of n mapping entries, traversal is generally O(n) time. The result stores the retained structure, so additional memory is proportional to the output plus recursion-stack depth. Deeply nested input can exceed Python’s recursion limit; an explicit stack-based traversal is safer for untrusted or machine-generated depth. Also remember that wanted should be a set (or another efficient membership collection) when it contains many keys; repeatedly searching a list makes membership O(k) for k wanted keys.

Testing the contract

Tests should assert policy, not just one happy-path value:

def test_nested_matches_and_ancestors():
    source = {"a": {"b": {"keep": 1, "drop": 2}}, "drop": 3}
    assert select_keys(source, {"keep"}) == {"a": {"b": {"keep": 1}}}


def test_matching_empty_value_is_kept():
    assert select_keys({"settings": {}}, {"settings"}) == {"settings": {}}


def test_nonmatching_empty_ancestor_is_removed():
    assert select_keys({"a": {"drop": 1}}, {"keep"}) == {}


def test_non_string_keys_work():
    assert select_keys({1: "one", "nested": {2: "two"}}, {2}) == {"nested": {2: "two"}}

Add tests for mapping subclasses, lists if supported, tuples, cycles, duplicate references, and very deep input when those cases are in scope.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common failures

“My nested result is missing its parent key.”

Your branch policy probably keeps only matching parent keys. Recurse first and retain a nonmatching parent when the filtered child is non-empty, as in select_keys.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“The function keeps empty dictionaries I wanted removed.”

Check the empty-branch condition. Use isinstance(value, dict) and value for unselected ancestors, or add an explicit keep_empty option to make the policy visible.

“A custom mapping is ignored.”

Replace isinstance(value, dict) with isinstance(value, Mapping) and decide how the output should be constructed.

“Keys inside a list are unchanged.”

The dictionary-only contract intentionally does not enter sequences. Use a sequence-aware walker and state whether lists and tuples are rebuilt.

“I get a recursion error.”

Inspect for cycles or excessive nesting. Reject cycles with an active-object set, or replace recursion with an explicit stack for inputs whose depth is not controlled.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“The source changed unexpectedly.”

Your selected values may be mutable objects shared with the source, or you may be using an in-place function. Return a new result and deep-copy values only when independent ownership is required.

Or skip the browser setup

If you are generating documentation screenshots for examples of nested-dictionary output, ScreenshotNeo can return a clean image or PDF from one request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options, including full-page and element capture, device and retina settings, custom CSS or JavaScript, waits, request blocking, authentication headers, cookies, geolocation, caching, signed links, asynchronous jobs, bulk capture, and usage reporting.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I pass a list of keys instead of a set?

Yes. The in operator works with any container, but a set is usually preferable for repeated membership checks because it is designed for fast lookup.

Does recursion preserve the original dictionary subclass?

Not in the shown implementations: they intentionally return built-in dict objects. Preserve a custom type only after defining and testing how that type should be constructed.

Will these functions filter dictionary keys inside arbitrary class attributes?

No. They inspect mapping values only. Traversing object attributes requires a separate, type-specific serialization or visitor policy.

Is there a standard-library function named recursive key selection?

No single standard-library function defines this exact policy. Treat the recipes as application code and document their traversal, branch, empty-value, and mutation rules.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.