Part 1 — Getting Started

This page covers everything you need to go from zero to a working NestedDictionary in a few minutes.

Installation

Install ndict-tools from PyPI using pip:

pip install ndict-tools

Or with uv:

uv add ndict-tools

The package requires Python 3.10 or later and has no runtime dependencies beyond the standard library.

Creating a nested dictionary

All three public classes share the same construction interface. The simplest way is to pass a plain dict:

from ndict_tools import NestedDictionary

nd = NestedDictionary({"project": {"name": "ndict-tools", "version": "1.1.0"}})

You can also pass any iterable of (key, value) pairs or a zip:

nd = NestedDictionary(zip(["a", "b"], [{"x": 1}, 2]))
nd = NestedDictionary([("a", {"x": 1}), ("b", 2)])

To convert a pre-existing plain dictionary — including deeply nested ones — use from_dict(). Because a plain dict carries no information about how missing keys or printing should behave, you must supply that configuration explicitly:

plain = {"europe": {"france": "Paris", "germany": "Berlin"}}
nd = NestedDictionary.from_dict(
    plain,
    default_setup={"indent": 2, "default_factory": NestedDictionary},
)

The default_setup dictionary accepts two keys:

  • indent — number of spaces used when printing (0 disables indentation).

  • default_factory — the class instantiated for missing keys; set to NestedDictionary for lenient behaviour or None for strict.

Reading, writing, and deleting

Standard single-key access works exactly like a plain dict:

nd = NestedDictionary({"a": {"b": 1}, "c": 2})

nd["a"]          # NestedDictionary({'b': 1})
nd["c"]          # 2

For multi-level access, pass a list of keys — a hierarchical key:

nd[["a", "b"]]         # 1

nd[["a", "b"]] = 99    # write — intermediate levels created automatically
nd[["a", "b"]]         # 99

del nd[["a", "b"]]     # delete — empty parent 'a' is cleaned up automatically
"a" in nd              # False

You can also chain standard attribute access:

nd["a"]["b"]    # equivalent to nd[["a", "b"]]

Searching across all levels

Unlike a plain dict, a NestedDictionary tracks every key at every depth. You can search without knowing the exact path:

nd = NestedDictionary({
    "europe": {"france": {"capital": "Paris"}},
    "asia":   {"japan":  {"capital": "Tokyo"}},
})

nd.is_key("capital")       # True  — exists somewhere in the tree
nd.occurrences("capital")  # 2     — appears at two different paths
nd.key_list("capital")     # [('europe', 'france', 'capital'),
                           #  ('asia', 'japan', 'capital')]
nd.items_list("capital")   # ['Paris', 'Tokyo']

Choosing the right variant

All three classes share the same interface. The only difference is what happens when you access a key that does not exist.

Class

Behaviour on unknown key

NestedDictionary

Returns a new empty NestedDictionary and records the access. Mirrors collections.defaultdict.

StrictNestedDictionary

Raises KeyError. Use when unknown keys should never go unnoticed — configuration parsing, validated payloads.

SmoothNestedDictionary

Returns a new empty SmoothNestedDictionary at any depth. Safe for deep chaining without prior key checks.

A quick rule of thumb: