Module 2 · Core Data Structures ⏱ 19 min

Dictionaries

By the end of this lesson you will be able to:
  • Create a dictionary that maps keys to values
  • Read and update values by key, safely with the get method
  • Iterate over a dictionary with keys, values, and items
  • Recognise when a dictionary is the right tool instead of a list
  • Avoid the KeyError trap when a key might be missing

Imagine you have a thousand prices and you need the cost of 'apple'. With a list, you scan every item until you find it — slow, and the code is a loop inside a loop. A dictionary solves this in one step: you look up the key, and Python jumps straight to the value.

A dictionary maps keys to values. You write it with curly braces, a colon between each key and value, and commas between pairs:

prices = {"apple": 2, "banana": 3, "cherry": 5}

Keys must be unique and hashable — strings and numbers work fine. Values can be anything: numbers, strings, lists, even other dictionaries. Think of a dict as a set of labelled boxes: the label is the key, and the contents are the value.

Because lookup is fast and does not slow down as the dict grows, dictionaries are the standard tool for counting, grouping, caching, and configuration. Whenever your code asks "what is the value for this name?", a dict is almost always the right answer.

flowchart LR
  K1["key 'apple'"] --> V1["value 2"]
  K2["key 'banana'"] --> V2["value 3"]
  K3["key 'cherry'"] --> V3["value 5"]
  style K1 fill:#3776ab,color:#fff
  style K2 fill:#3776ab,color:#fff
  style K3 fill:#3776ab,color:#fff
Each key points to one value. Duplicate keys are impossible — the last one wins.

Reading and writing

Use square brackets to read a value or assign one:

print(prices["apple"])    # 2
prices["apple"] = 1       # update an existing key
prices["date"] = 4        # add a brand-new key

Assigning to an existing key overwrites the old value — there is no warning, because uniqueness is the whole point of a dictionary. Assigning to a new key creates it on the spot; dictionaries grow dynamically.

But here is the trap every beginner hits: if the key does not exist, prices["kumquat"] crashes with a KeyError. That is fine when you know the key is there, but when it might be missing — user input, configuration files, or data from another system — you need a safer approach.

One rule of thumb: d[key] is for "I am certain this key exists". When you are not certain, reach for .get() instead.

Create a dictionary, read a value, update it, and add a new key.
prices = {"apple": 2, "banana": 3, "cherry": 5}
print(prices["apple"])

prices["apple"] = 1
prices["date"] = 4
print(prices)

The safe way: .get() and membership testing

.get(key, default) returns the value if the key exists, or the default if it does not. No crash, no try/except needed:

print(prices.get("apple", 0))       # 1 — apple is there
print(prices.get("kumquat", 0))     # 0 — kumquat is missing

You can also test whether a key exists with the in keyword. "apple" in prices is True; "kumquat" in prices is False. This is fast — dictionaries are built for quick lookups — and it reads like plain English.

These two tools together cover almost every safe-access pattern. Use if key in d when you need a branch, and use .get() when you need a fallback value in one expression. For example, if fruit in prices: print(prices[fruit]) is safe because you checked first; prices.get(fruit, 0) is safe because it supplies its own default.

Safe lookups with .get() and membership testing with in.
prices = {"apple": 2, "banana": 3}
print(prices.get("apple", 0))
print(prices.get("dragonfruit", 0))
print("apple" in prices)
print("dragonfruit" in prices)

# Safe branch
for fruit in ["apple", "dragonfruit"]:
    if fruit in prices:
        print(fruit, "is", prices[fruit])
    else:
        print(fruit, "not available")
flowchart TD
  S["prices.get('apple', 0)"] --> C{"key exists?"}
  C -->|yes| V["return value 2"]
  C -->|no| D["return default 0"]
  style S fill:#3776ab,color:#fff
  style V fill:#16a34a,color:#fff
  style D fill:#b45309,color:#fff
.get() checks the key and returns the value if found, or the default if not.

Iterating over dictionaries

Three methods give you every entry, each from a different angle:

  • .keys() — every key.
  • .values() — every value.
  • .items() — every (key, value) pair.

Most of the time you want .items(), because it gives you both pieces at once and you can unpack them into two variables:

for fruit, price in prices.items():
    print(fruit, "costs", price)

Looping order matches insertion order in Python 3.7+, which is the version you are using. That predictability makes dictionaries reliable for ordered data too.

If you only need the values — for example, to sum every price — loop over .values() directly. If you only need the keys, use .keys(). Each method is clearer than looping over the dict and indexing back in. You can also wrap any of them in list() if you need a real list instead of a view.

Iterate with .keys(), .values(), and .items().
prices = {"apple": 2, "banana": 3, "cherry": 5}

print("Keys:", list(prices.keys()))
print("Values:", list(prices.values()))
print("Total:", sum(prices.values()))

for fruit, price in prices.items():
    print(fruit, "costs", price)
flowchart LR
  D["{'apple': 2, 'banana': 3}"] --> I[".items()"]
  I --> P1["fruit='apple'\nprice=2"]
  I --> P2["fruit='banana'\nprice=3"]
  style D fill:#3776ab,color:#fff
  style I fill:#1e293b,color:#fff
Looping with .items() unpacks each key-value pair as you go.
Exercise

Write word_lengths(words) that returns a dictionary mapping each word to its length. (Example: word_lengths(["cat", "hi"]){"cat": 3, "hi": 2}.)

def word_lengths(words):
    result = {}
    # store len(w) for each word w
    return result
Exercise

What does this print? items() gives each key and value as a pair.

prices = {"apple": 2, "banana": 3}
for fruit, price in prices.items():
    print(fruit, price)
Exercise

Write merge_counts(a, b) that takes two dictionaries mapping strings to integers and returns a new dictionary with the same keys, where each value is the sum of the values from a and b. If a key is missing from one dict, treat it as 0. Use .get() to stay safe.

def merge_counts(a, b):
    # return a new dict with summed values
    pass
Exercise

This function is supposed to return the price of a fruit, or 0 if it's not in the menu. It crashes on missing keys. Fix it using .get().

def fruit_price(menu, fruit):
    return menu[fruit]
Exercise

Write invert_dict(d) that takes a dictionary and returns a new dictionary with keys and values swapped. If a value appears more than once, the last key wins. (Example: invert_dict({"a": 1, "b": 2}){1: "a", 2: "b"}.)

def invert_dict(d):
    # swap keys and values
    pass

Recap

  • A dictionary maps keys to values in one step.
  • d[key] reads or writes, but crashes with KeyError if the key is missing.
  • .get(key, default) is the safe alternative that never crashes.
  • Use key in d to test membership without fetching the value.
  • .items() is the most common way to loop, unpacking into key, value pairs.
  • Dictionary lookup is fast and stays fast as the data grows — when you need to find things by name, reach for a dict before a list.

Next you'll learn to handle the errors that happen when code goes wrong, including what to do when a KeyError really does escape.

Checkpoint quiz

How do you read key 'a' safely, returning 0 if it's missing?

What does len({'a': 1, 'b': 2}) return?

Go deeper — technical resources