Nested Dictionaries¶
Python’s built-in dict can hold any value — including another
dict. This natural nesting has no dedicated type in the standard
library, which makes common tasks (checking whether a key exists anywhere in
the hierarchy, counting its occurrences, or converting the whole structure to
JSON) unnecessarily verbose.
ndict-tools fills this gap by providing three ready-to-use classes built
on top of collections.defaultdict.
What is a nested dictionary?¶
A nested dictionary is a dictionary whose values may themselves be dictionaries, to any depth:
data = {
"project": {
"name": "ndict-tools",
"version": "1.1.0",
"authors": {
"lead": "biface",
},
},
"license": "MIT",
}
Accessing data["project"]["authors"]["lead"] requires knowing the exact
path in advance. With ndict-tools, you can also write:
from ndict_tools import NestedDictionary
nd = NestedDictionary(data)
nd[["project", "authors", "lead"]] # 'biface'
nd[["license"]] # 'MIT'
The list notation is a hierarchical key: each element is a level in the nesting.
The three public classes¶
All three classes accept the same construction patterns — a plain
dict, a zip, or a list of (key, value) pairs:
from ndict_tools import NestedDictionary
# From a plain dict
nd = NestedDictionary({"a": {"b": 1}, "c": 2})
# From a zip
nd = NestedDictionary(zip(["a", "b"], [{"x": 1}, 2]))
# From a list of pairs
nd = NestedDictionary([("a", {"b": 1}), ("c", 2)])
The difference between the three classes lies in how they respond to an unknown key.
NestedDictionary — lenient by default¶
Accessing a missing key silently creates a new, empty
NestedDictionary at that location. This mirrors the
behaviour of collections.defaultdict.
from ndict_tools import NestedDictionary
nd = NestedDictionary({"a": 1})
nd["missing"] # returns an empty NestedDictionary, does not raise
nd["missing"]["deeper"] = 42 # works — intermediate levels are created
StrictNestedDictionary — raises on missing keys¶
Accessing a missing key raises a KeyError, just like a plain
dict. Use this class when unknown keys should never go unnoticed.
from ndict_tools import StrictNestedDictionary
nd = StrictNestedDictionary({"a": 1})
nd["missing"] # raises KeyError
SmoothNestedDictionary — always returns a nested dict¶
Accessing a missing key returns a new, empty
SmoothNestedDictionary. Unlike NestedDictionary,
every returned object is guaranteed to be a SmoothNestedDictionary,
making deep chaining safe regardless of depth.
from ndict_tools import SmoothNestedDictionary
nd = SmoothNestedDictionary({"a": 1})
result = nd["missing"]["even"]["deeper"] # returns empty SmoothNestedDictionary
Hierarchical key access¶
Any NestedDictionary (and its subclasses) accepts a
Python list as a key. Reading and writing both work:
from ndict_tools import NestedDictionary
nd = NestedDictionary({"a": {"b": {"c": 42}}})
# Reading
nd[["a", "b", "c"]] # 42
nd[["a", "b"]] # NestedDictionary({'c': 42})
# Writing — intermediate levels are created automatically
nd[["x", "y", "z"]] = 99
nd["x"]["y"]["z"] # 99
# Deleting — empty parents are cleaned up automatically
del nd[["x", "y", "z"]]
"x" in nd # False
Constructing from an existing dict¶
The from_dict() class method provides
an alternative constructor that recursively converts a plain dictionary.
Because a plain dict carries no information about how missing keys
should behave or how the structure should be printed, you must supply that
configuration explicitly via default_setup:
indent— number of spaces used when printing the dictionary (0disables indentation).default_factory— the class instantiated for missing keys (set toNestedDictionaryfor lenient behaviour,Nonefor strict behaviour).
from ndict_tools import NestedDictionary
plain = {"region": {"country": {"city": "Paris"}}}
nd = NestedDictionary.from_dict(
plain,
default_setup={"indent": 2, "default_factory": NestedDictionary},
)
nd[["region", "country", "city"]] # 'Paris'
Searching across all levels¶
Because all keys are tracked in the hierarchy, you can search without knowing the depth:
from ndict_tools import NestedDictionary
nd = NestedDictionary({
"europe": {"france": {"capital": "Paris"}},
"asia": {"japan": {"capital": "Tokyo"}},
})
nd.is_key("capital") # True — exists somewhere
nd.occurrences("capital") # 2 — appears twice
nd.key_list("capital") # [('europe', 'france', 'capital'),
# ('asia', 'japan', 'capital')]
nd.items_list("capital") # ['Paris', 'Tokyo']
Serialisation¶
A NestedDictionary can be saved to and restored from
JSON or pickle without any extra setup:
from ndict_tools import NestedDictionary
nd = NestedDictionary({"a": {"b": 1}})
# JSON round-trip
nd.to_json("/tmp/nd.json")
nd2 = NestedDictionary.from_json(
"/tmp/nd.json",
default_setup={"indent": 0, "default_factory": NestedDictionary},
)
# Pickle round-trip (SHA-256 sidecar written automatically)
nd.to_pickle("/tmp/nd.pkl")
nd3 = NestedDictionary.from_pickle("/tmp/nd.pkl")