The Quiet Terminal

← Beginner

Dictionaries: looping, updating, and surviving keys

2026-07-01 · 3 min read

I use dictionaries the way some people use sticky notes: for anything that has a name. Device names to IP addresses, setting names to values, month numbers to names. The basics fit in one post — including the mistake I made for months, and the KeyError that finally taught me the safe version.

Looping with .items()

Here's my home-network cheat sheet as a dict. When I want both the name and the address, .items() hands over both at once:

devices = {"router": "192.168.1.1", "nas": "192.168.1.20", "printer": "192.168.1.9"}

for name, ip in devices.items():
    print(f"{name:<8} {ip}")
router   192.168.1.1
nas      192.168.1.20
printer  192.168.1.9

(The :<8 pads the name to eight characters — the same f-string trick as always.) For a long time I wrote for name in devices: and then looked up devices[name] inside the loop. It works, but .items() says what it means.

.get(): asking with a fallback

The settings-file pattern — some keys are always there, some are optional:

settings = {"theme": "dark", "autosave": True}

theme = settings.get("theme", "light")
volume = settings.get("volume", 50)     # not in settings — 50, no crash

.get(key, default) returns the value if the key exists and your default if it doesn't. The line reads the same both ways, which is the point: optional settings stop being a special case.

One more tool in the same family: "laptop" in devices is a membership test that costs nothing and answers yes or no. Between in, .get(), and letting a KeyError through on purpose, I can say three different things about a key — might be there, probably is, and must be.

Updating and merging

Square brackets add or update — same line either way:

devices["printer"] = "192.168.1.10"    # update an existing key
devices["camera"] = "192.168.1.14"     # add a new one

defaults = {"retries": 3, "timeout": 30}
user_prefs = {"timeout": 10}
config = {**defaults, **user_prefs}    # {'retries': 3, 'timeout': 10}

The {**a, **b} merge builds a new dict from both, and later values win — exactly what you want when user preferences override defaults.

A taste of nesting

Dicts hold whatever you put in them, including other dicts. Config-shaped data lives happily two levels deep:

host = {
    "name": "nas",
    "ip": "192.168.1.20",
    "disks": {"/": "78%", "/data": "41%"},
}

print(host["disks"]["/data"])      # 41%

Square brackets chain left to right: host, then its disks dict, then the key inside it. Three levels deep is usually where I start wanting real objects — but for config-shaped data, nesting stays readable longer than you'd expect.

The KeyError moment

And then one day I asked for a device that isn't in the dict:

print(devices["laptop"])
KeyError: 'laptop'

The message is the exact missing key, which makes KeyError one of the friendliest errors once you know how to read a traceback from the bottom up — I wrote a whole post about that. My rule now: if a key might legitimately be missing, an optional setting or a device that's offline, use .get(). If a missing key means my data is broken, let the KeyError happen, loudly. Silence isn't the same as handling.