Python – Dictionaries & Sets

July 18, 20264 min readUpdated 8/20/2026

The dictionary is the data structure Python is built out of — objects, modules and namespaces are all dicts underneath. The set is its quieter sibling and answers two questions in one line that would otherwise take a loop. This post covers both.

A dict maps keys to values

account = {
    "type": "Checking",
    "number": "1001-0001",
    "balance": 1250.00,
}

print(account["type"])          # Output: Checking
print(len(account))             # Output: 3
print("balance" in account)     # Output: True

account["balance"] = 1350.00    # update
account["opened"] = "2020-03-06"  # add
print(account["balance"])       # Output: 1350.0

Assigning to a key that exists updates it; assigning to one that does not adds it. There is no separate "add" and "update" call, which is convenient and occasionally hides a typo — misspell a key name and you silently create a second entry rather than getting an error.

Since Python 3.7 a dict keeps insertion order, so looping over one is repeatable. Do not rely on it for sorting, only for stability.

Reading a key that might not be there

Square brackets raise when the key is missing:

account = {"type": "Checking"}
account["balance"]

That is a KeyError, and it is the right behaviour when the key should be there — you want to know. When it is genuinely optional, use get():

account = {"type": "Checking"}

print(account.get("balance"))          # Output: None
print(account.get("balance", 0.0))     # Output: 0.0
print(account.get("type", "Unknown"))  # Output: Checking

get() returns None for a missing key, or the default you supply. The two-argument form is the one you will use most — it collapses a four-line if key in d: ... else: ... into an expression.

The distinction is worth being deliberate about. d[key] says "this must exist"; d.get(key, default) says "this is optional". Using get() everywhere turns a missing-key bug into a None that travels three functions before failing somewhere confusing.

Looping over one

balances = {"Checking": 1250.00, "Savings": 8400.50}

for name, balance in balances.items():
    print(f"{name:<10}{balance:>10,.2f}")

# Output: Checking    1,250.00
# Output: Savings     8,400.50

print(list(balances.keys()))     # Output: ['Checking', 'Savings']
print(sum(balances.values()))    # Output: 9650.5

Iterating the dict itself gives keys, which is why .items() exists and why you should reach for it by reflex. sum(d.values()) is the one-line total.

Counting and grouping

These two jobs are most of what dicts get used for in practice.

from collections import Counter, defaultdict

kinds = ["Deposit", "Withdrawal", "Deposit", "Transfer", "Deposit"]

print(Counter(kinds).most_common(2))
# Output: [('Deposit', 3), ('Withdrawal', 1)]

by_letter = defaultdict(list)
for name in ["Alice", "Bob", "Anna"]:
    by_letter[name[0]].append(name)

print(dict(by_letter))     # Output: {'A': ['Alice', 'Anna'], 'B': ['Bob']}

Counter counts anything iterable and most_common(n) ranks it. defaultdict(list) creates an empty list the first time you touch a key, so the "if the key is not there yet, add an empty list" line disappears. Both live in collections and both replace code people write by hand for years before meeting them.

Removing, and setdefault

account = {"type": "Checking", "balance": 1250.00, "temp": "x"}

del account["temp"]                       # raises if missing
removed = account.pop("balance", None)    # returns it; default if missing
print(removed)                            # Output: 1250.0
print(account.pop("nothing", "absent"))   # Output: absent
print(account)                            # Output: {'type': 'Checking'}

del when the key must be there, pop(key, default) when it might not be — the same "must exist" versus "optional" distinction as [] and get().

setdefault() reads the key and inserts a default if it was absent, in one step:

grouped = {}
for name in ["Alice", "Bob", "Anna"]:
    grouped.setdefault(name[0], []).append(name)

print(grouped)      # Output: {'A': ['Alice', 'Anna'], 'B': ['Bob']}

Same result as the defaultdict above. Use setdefault for a one-off and a defaultdict when the whole dict works that way — mixing them in one function is how you end up unsure which behaviour applies.

Dict comprehensions

balances = {"Checking": 1250.00, "Savings": 8400.50, "Credit": -320.75}

print({k: round(v * 1.05, 2) for k, v in balances.items()})
# Output: {'Checking': 1312.5, 'Savings': 8820.52, 'Credit': -336.79}

print({k: v for k, v in balances.items() if v > 0})
# Output: {'Checking': 1250.0, 'Savings': 8400.5}

print({v: k for k, v in balances.items()})
# Output: {1250.0: 'Checking', 8400.5: 'Savings', -320.75: 'Credit'}

Same shape as a list comprehension with key: value in front. The third one inverts the dict — handy, and lossy if two keys shared a value.

Merging

defaults = {"currency": "USD", "overdraft": 0}
settings = {"overdraft": 500}

print(defaults | settings)        # Output: {'currency': 'USD', 'overdraft': 500}
print({**defaults, **settings})   # Output: {'currency': 'USD', 'overdraft': 500}

The | operator arrived in 3.9 and is the readable one. Right-hand side wins on a clash, which is what you want for "defaults, then overrides".

Sets

A set is an unordered collection with no duplicates. It answers "is this in here" in constant time and de-duplicates for free:

emails = ["a@x.com", "b@x.com", "a@x.com"]

unique = set(emails)
print(len(unique))                 # Output: 2
print("a@x.com" in unique)         # Output: True

admins = {"a@x.com", "c@x.com"}
print(sorted(unique & admins))     # Output: ['a@x.com']
print(sorted(unique | admins))     # Output: ['a@x.com', 'b@x.com', 'c@x.com']
print(sorted(unique - admins))     # Output: ['b@x.com']

& is intersection, | union, - difference. "Which users are in both lists" is one character instead of a nested loop. Note the sorted() in each print — a set has no order, so printing one directly gives you an arrangement you cannot rely on.

{} is an empty dict, not an empty set. For that you need set().

What can be a key

Keys and set members must be hashable, which in practice means immutable. Strings, numbers, booleans and tuples qualify. Lists, dicts and sets do not:

{["a", "b"]: 1}

That is TypeError: unhashable type: 'list', and it is the error that sends people to tuples for compound keys. The reason is that a dict finds a value by hashing the key; if the key could change afterwards, the entry would be lost in a bucket nobody looks in.

Next: Functions.