Python Dictionaries: Keys, Values and Lookup

A dictionary maps keys to values and finds any value in roughly constant time. It is the most used container in Python and the structure most real data arrives in.

Creating a dictionary

empty = {}
empty_too = dict()

person = {"name": "Meera", "age": 27, "city": "Pune"}

from_pairs = dict([("a", 1), ("b", 2)])
from_kwargs = dict(name="Meera", age=27)
from_zip = dict(zip(["a", "b"], [1, 2]))
from_keys = dict.fromkeys(["a", "b"], 0)     # {'a': 0, 'b': 0}

print(person)
dict.fromkeys(keys, []) gives every key the same list object. Appending through one key changes them all. Use a comprehension instead when the default is mutable.

Keys must be hashable

valid = {
    "text": 1,
    42: "a number key",
    (0, 0): "a tuple key",
    True: "a boolean key",
    None: "a None key",
}

# invalid = {[1, 2]: "x"}      # TypeError: unhashable type: 'list'
# invalid = {{"a": 1}: "x"}    # TypeError: unhashable type: 'dict'

Values may be anything at all, including lists, dictionaries and functions. Only keys are restricted.

Reading a value

person = {"name": "Meera", "age": 27}

print(person["name"])            # Meera
# print(person["email"])         # KeyError: 'email'

print(person.get("email"))               # None - no error
print(person.get("email", "unknown"))    # unknown - your own default
Key presentKey missingUse when
d[key]the valueKeyErrorThe key must exist; a missing one is a bug
d.get(key)the valueNoneAbsence is normal
d.get(key, x)the valuexYou have a sensible fallback

Do not reach for get automatically. If a missing key means your data is wrong, the KeyError is doing you a favour by stopping immediately.

Adding and updating

person = {"name": "Meera"}

person["age"] = 27               # add a new key
person["name"] = "Meera Nair"    # overwrite an existing one
print(person)

person.update({"city": "Pune", "age": 28})    # add or overwrite several
person.update(role="engineer")                # keyword form
print(person)

# Merge, Python 3.9 and later
defaults = {"theme": "light", "size": 12}
overrides = {"size": 14}
print(defaults | overrides)      # {'theme': 'light', 'size': 14}

defaults |= overrides            # in place merge
print(defaults)

On a duplicate key, the value on the right wins. That is what makes this the standard way to apply overrides on top of defaults.

Removing

person = {"name": "Meera", "age": 27, "city": "Pune", "temp": 1}

age = person.pop("age")               # removes and returns
print(age)                            # 27

missing = person.pop("email", None)   # a default stops the KeyError
print(missing)                        # None

del person["temp"]
# del person["nope"]                  # KeyError

last = person.popitem()               # removes and returns the LAST pair
print(last)

person.clear()
print(person)                         # {}

Checking for a key

person = {"name": "Meera", "age": None}

print("name" in person)          # True
print("email" not in person)     # True
print(27 in person)              # False - `in` checks KEYS, not values
print(27 in person.values())     # for values, say so

# A present key with a falsy value
print("age" in person)           # True
print(person.get("age"))         # None - present, but empty

Order is guaranteed

d = {}
d["z"] = 1
d["a"] = 2
d["m"] = 3
print(list(d))         # ['z', 'a', 'm'] - insertion order, always

Since Python 3.7 dictionaries preserve insertion order as a language guarantee. Overwriting a value keeps the key in its original position; deleting and re-adding moves it to the end.

Iterating

ages = {"Meera": 27, "Arun": 31, "Sara": 24}

for key in ages:                     # keys by default
    print(key)

for key in ages.keys():              # explicit, same thing
    print(key)

for value in ages.values():
    print(value)

for key, value in ages.items():      # the usual form
    print(f"{key} is {value}")

for name, age in sorted(ages.items(), key=lambda pair: pair[1]):
    print(name, age)                 # sorted by age

The views are live

ages = {"Meera": 27}
keys = ages.keys()

ages["Arun"] = 31
print(list(keys))          # ['Meera', 'Arun'] - the view updated itself

print(ages.keys() & {"Meera", "Zara"})    # {'Meera'} - key views act like sets

Do not resize while iterating

scores = {"a": 1, "b": 0, "c": 2}

# for key in scores:
#     if scores[key] == 0:
#         del scores[key]          # RuntimeError: dictionary changed size

for key in list(scores):           # iterate over a snapshot of the keys
    if scores[key] == 0:
        del scores[key]
print(scores)                      # {'a': 1, 'c': 2}

# Or build a new dictionary
scores = {k: v for k, v in scores.items() if v != 0}

Nested dictionaries

users = {
    "meera": {"age": 27, "roles": ["admin", "editor"]},
    "arun": {"age": 31, "roles": ["viewer"]},
}

print(users["meera"]["age"])                 # 27
print(users["meera"]["roles"][0])            # admin

print(users.get("zara", {}).get("age"))      # None - safe at both levels

for name, details in users.items():
    print(f"{name}: {details['age']}, {', '.join(details['roles'])}")

Common mistakes

  • Using d[key] on data that may not contain the key.
  • Using get everywhere, hiding genuine bugs behind a None.
  • Assuming in searches values.
  • Deleting keys while iterating.
  • Using dict.fromkeys(keys, []) and sharing one list across every key.
  • Using a list as a key.

Best practices

  • Use d[key] when the key must exist, get when it may not.
  • Use .items() whenever the loop needs both parts.
  • Iterate over list(d) when the loop will delete keys.
  • Use | to layer overrides on defaults.
  • Keep nesting shallow. Past two levels, a class or a dataclass usually reads better.

Practice

  1. Build a dictionary of five products and prices, then print them sorted by price.
  2. Explain the difference between d["x"] and d.get("x") for a missing key, and when each is right.
  3. Merge a defaults dictionary with a user settings dictionary so that user settings win.
  4. Remove every entry with a zero value, in two different ways.
  5. Explain why dict.fromkeys(["a", "b"], []) is dangerous, and give a safe alternative.

Conclusion

A dictionary maps hashable keys to any values, keeps insertion order, and finds a value without scanning. Choose between [] and get deliberately, iterate with .items(), and never change the size of a dictionary you are looping over.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Trees and Graphs

A tree is a graph with no cycles and one root. Both are walked with the same two strategies - depth first with a stack, breadth first with a queue.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.