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.

To select keys throughout a nested dictionary, walk each dictionary’s key-value pairs, decide whether each key should be kept, and recursively inspect values that are dictionaries. The key design choice is what happens to a parent branch when a selected key appears deeper inside it. The example below returns a new dictionary, retains ancestors of matching nested keys, and filters nested dictionaries even when their parent key matches.

A recursive selector with an explicit contract

Python dictionaries can hold arbitrary values, so recursion does not happen automatically. This function descends into dictionary values only; it does not traverse lists or tuples. It accepts an iterable of keys to match by exact membership, returns ordinary dictionaries, leaves non-dictionary values unchanged, and keeps a nonmatching parent branch when it contains selected keys below it. Empty branches with no selected key are omitted.

def select_keys(data, wanted):
    """Return selected keys and their matching nested dictionary branches.

    Recurses into dict values only. Keeps a branch when it contains
    a selected key at any depth. Does not mutate data.
    """
    wanted = set(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


record = {
    "name": "Mina",
    "profile": {
        "email": "[email protected]",
        "address": {"city": "Oslo", "postcode": "0150"},
    },
    "settings": {"theme": "dark"},
}

selected = select_keys(record, {"email", "city"})
print(selected)
# {'profile': {'email': '[email protected]',
#              'address': {'city': 'Oslo'}}}

In this result, profile and address remain even though neither is selected: each contains a selected descendant. The name and settings branches disappear because neither their keys nor any descendant keys match. Since a fresh dictionary is built at every visited dictionary, the input is not modified.

Why recurse before deciding what to keep?

The function first filters a dictionary-valued value, then evaluates the current key. That ordering means that if profile itself is in wanted, its nested dictionary is still filtered rather than copied wholesale. If matching a key should instead preserve its entire value untouched, the policy must change: test membership first and copy the value directly for matching keys. That simpler policy is appropriate when “select key” means “keep this key and everything under it,” rather than “filter matching keys at every level.”

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

Choose the branch and value policies

Keep ancestors, or only matching keys?

The implementation above keeps ancestors of matches so selected descendants remain reachable in the output. An alternative is to retain only keys that match, without retaining nonmatching containers. That would make a nested match inaccessible unless the result’s shape were flattened or represented differently. For most nested-dictionary filters, preserving ancestor branches is the useful behavior.

The example drops empty dictionaries created by filtering, because it only retains a nonmatching dictionary-valued key when the filtered result is nonempty. If empty branches carry meaning in your data, remove the and value test:

elif isinstance(value, dict):
    result[key] = value

With that change, a nonmatching branch may remain as {} even when it contains no selected key. Decide whether an empty object represents useful structure or noise in the particular data format.

Dictionary values versus lists and tuples

The function intentionally treats lists, tuples, sets, and other objects as opaque values. For example, a value like {"events": [{"email": "[email protected]"}]} remains unchanged if events matches, and is not searched if it does not. Descending through sequences requires a separate rule: should each nested dictionary be filtered, should the sequence type be preserved, and what should happen to elements that become empty? Do not add generic container recursion without answering those questions; different containers have different semantics.

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

Exact keys, predicates, and non-string keys

Here, wanted is converted to a set for efficient membership checks, and keys match by ordinary Python equality and hashing. Keys need not be strings: dictionaries can use other hashable key types. If your data uses heterogeneous keys, provide matching values of the appropriate types. A string "1" and integer 1 are distinct keys.

For a rule such as “keep every key beginning with user_,” use a predicate instead of a membership set:

def select_keys_where(data, keep_key):
    result = {}

    for key, value in data.items():
        if isinstance(value, dict):
            value = select_keys_where(value, keep_key)

        if keep_key(key):
            result[key] = value
        elif isinstance(value, dict) and value:
            result[key] = value

    return result

result = select_keys_where(
    {"account": {"user_id": 7, "email": "[email protected]", "prefs": {"user_theme": "dark"}}},
    lambda key: isinstance(key, str) and key.startswith("user_"),
)

The isinstance guard matters when keys are not guaranteed to be strings; calling startswith on an integer would raise AttributeError.

Supporting Mapping implementations

Use dict when the function’s contract is deliberately limited to built-in dictionaries and their subclasses. Python documents dict as its standard mapping type, with keys that are hashable and values that may be arbitrary objects (Python 3.13 Built-in Types documentation). An isinstance(value, dict) check also accepts subclasses; it is broader than checking type(value) is dict (Python 3.13 Built-in Functions documentation).

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

If the input may include custom mapping objects, use the abstract interface in collections.abc:

from collections.abc import Mapping

def select_mapping_keys(data, wanted):
    wanted = set(wanted)
    result = {}

    for key, value in data.items():
        if isinstance(value, Mapping):
            value = select_mapping_keys(value, wanted)

        if key in wanted:
            result[key] = value
        elif isinstance(value, Mapping) and value:
            result[key] = value

    return result

Mapping describes a mapping interface with __getitem__, __iter__, and __len__, along with mixin operations such as items and get (Python 3.12.14 collections.abc documentation). This version accepts more mapping implementations, but still constructs ordinary dictionaries. If callers require output to retain custom mapping types, define how each type is rebuilt; there is no safe universal assumption that calling {} or the input type’s constructor preserves its behavior.

Run and test the function safely

  1. Decide the contract. Specify mapping types, which values are traversed, whether ancestors remain, whether empty branches remain, and whether selected parent values are themselves filtered.
  2. Use a small example with a match at multiple depths. Include a matching key whose value is another dictionary, a nonmatching branch, and a nested match under a nonmatching parent.
  3. Check that the source stays unchanged. The implementation above creates new dictionaries for the mappings it visits, but it does not deep-copy opaque values such as lists or custom objects.
  4. Check edge cases. Try an empty input, an empty selection set, and a key whose matching value is {}. With the shown policy, an empty dictionary under a selected key is retained; a nonmatching empty branch is discarded.
source = {"outer": {"keep": 1, "drop": 2}, "drop": 3}
result = select_keys(source, {"keep"})

assert result == {"outer": {"keep": 1}}
assert source == {"outer": {"keep": 1, "drop": 2}, "drop": 3}
assert select_keys({}, {"keep"}) == {}
assert select_keys({"x": 1}, set()) == {}

Cycles, shared objects, and recursion limits

Nested JSON-like data is normally a tree: each child leads to a finite object that does not point back to an ancestor. General Python objects need not have that shape. A dictionary can contain itself, directly or indirectly, causing this straightforward recursive function to continue until Python raises RecursionError. Deeply nested input can reach the recursion limit even without a cycle.

For untrusted or arbitrary object graphs, choose an explicit policy. You can reject cyclic inputs, track object identities currently on the recursion path to detect cycles, or implement an iterative traversal with a stack. A global “already visited” set has a different effect: it can treat a shared dictionary referenced in two places as if the second occurrence were a cycle and omit or reuse it. If shared references must be preserved, the output policy needs to track source-to-output objects, not just visited identities.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common mistakes and fixes

  • Only filtering the root: a comprehension such as {k: v for k, v in data.items() if k in wanted} never examines nested dictionaries. Call the recursive function on dictionary values.
  • Losing nested matches: keeping only matching keys drops the nonmatching ancestors needed to reach a nested selection. Retain nonempty filtered branches, as in the example.
  • Keeping an entire selected subtree unintentionally: recurse into dictionary values before testing whether their parent key matches.
  • Unexpected list contents: the dictionary-only contract leaves lists untouched. Add list traversal only after specifying element handling and output type preservation.
  • Type errors with predicates: test key types before string operations or comparisons that assume strings.
  • Mutating while iterating: modifying the same dictionary’s keys during iteration can cause errors and surprising behavior. Build a new result, or iterate over a stable copy if mutation is truly required.
  • Custom mappings disappear as custom types: accepting Mapping broadens inputs but this implementation returns dict. Add a deliberate reconstruction rule if type preservation is required.
  • Cycles or excessive depth: validate that inputs are finite trees, add cycle handling, or use an iterative traversal for deep structures.

Or skip the browser setup

Recursive dictionary selection is a local Python operation, so ScreenshotNeo is not a replacement for this function. For a separate developer task—capturing a web page as an image or PDF—ScreenshotNeo accepts a URL in one GET request:

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)

See the ScreenshotNeo API documentation for request options. It removes cookie/consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages and failed loads are not billed. An MCP server lets AI agents use its screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo or sign up free.

Frequently asked questions

Does this function alter the original dictionary?

No. It builds new dictionaries for traversed mappings. Other objects held as values are passed through unchanged rather than copied.

Is there a built-in recursive dictionary key selector?

The cited Python documentation describes dictionaries, mappings, and related operations, but does not document a standard-library function dedicated to recursive key selection. Treat the code here as an application-specific recipe and define its contract for your data.

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

What happens when wanted is empty?

No key matches, so the result is empty under the shown branch 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.