Skip to content

Python lesson 9 of 9

Python Dictionaries

Learn how Python dictionaries map keys to values, read them safely with .get(), add, update and delete entries, loop with .items(), and why lookups are fast.

Published · Every example on this page was run before it was published.

A list is great when the thing you care about is position — the first score, the last item, everything from index 2 onward. But a lot of real data has no meaningful position at all. If you store a person's phone number, you don't want to remember that it lives at index 47; you want to look it up by their name. When the natural way to find a value is by a label rather than by a slot number, you want a dictionary.

What is a Dictionary?

A dictionary is a collection that stores pairs: each key (the label you look something up by) points to a value (the data stored under that label). Together one key and its value are called an entry, or an item. Instead of asking "what's at position 3?", you ask "what's stored under "Grace"?"

The real-world analogy is the object it's named after. In a paper dictionary you never read from page one looking for the word gravity — you jump straight to the G section and find the word, and next to it sits its definition. The word is the key, the definition is the value, and the whole point of the book's design is that finding an entry doesn't get slower just because the dictionary is thick. A phone contact list works the same way: you type a name, you get a number back.

Python writes a dictionary with curly braces {}, each entry written as key: value, and the entries separated by commas:

Python
phone_book = {"Ada": "555-0101", "Grace": "555-0142", "Linus": "555-0199"}
print(phone_book["Grace"])
Output
555-0142

Notice what did not happen there: no loop, no index, no searching. You handed Python a key and got the matching value straight back. That one move — label in, value out — is what dictionaries exist to do, and every other feature in this lesson builds on it.

Creating Dictionaries and Reading Values

An empty dictionary is an empty pair of braces. Keys don't have to be strings and values can be any type at all, including numbers, booleans, and lists:

Python
empty = {}
ages = {"Ana": 31, "Ben": 24}
settings = {"volume": 7, "muted": False, "profile": "dark"}
print(len(empty))
print(len(ages))
print(settings)
Output
0
2
{'volume': 7, 'muted': False, 'profile': 'dark'}

len() on a dictionary counts entries, not keys and values separately — ages holds two entries, so len(ages) is 2. Printing a dictionary shows it back in the same brace notation you typed, and in modern Python (version 3.7 and later) entries stay in the order you inserted them, which is why settings prints with volume first.

There are two ways to read a value out, and the difference between them matters more than almost anything else in this lesson.

The first is square brackets, the same syntax lists use — except you put a key inside instead of an index. The second is the .get() method, which does the same lookup but hands back a fallback value instead of crashing when the key isn't there:

Python
stock = {"apples": 12, "pears": 4}
print(stock["apples"])
print(stock.get("pears"))
print(stock.get("plums"))
print(stock.get("plums", 0))
Output
12
4
None
0

Look closely at the last two lines. stock.get("plums") asked for a key that doesn't exist and calmly returned None — Python's built-in "no value here" object. stock.get("plums", 0) did the same lookup but supplied a default: a second argument that .get() returns when the key is missing. Since a missing fruit sensibly means "we have zero of those," 0 is a much more useful answer than None.

Square brackets behave very differently when the key isn't there. They raise a KeyError, which is Python's way of saying "you asked for a key I don't have," and an unhandled error stops the program on the spot:

Python
stock = {"apples": 12, "pears": 4}
try:
    print(stock["plums"])
except KeyError as error:
    print("KeyError for key:", error)
Output
KeyError for key: 'plums'

The try / except block above catches the error so the lesson can print it instead of crashing; without it, the program would simply stop. So which should you use? Use square brackets when the key must exist and its absence means something has genuinely gone wrong — you want the loud failure. Use .get() with a sensible default whenever a missing key is a normal, expected situation, such as counting things you haven't counted yet.

Adding, Updating, and Removing Entries

Dictionaries are mutable, meaning you can change their contents after creating them. Adding a new entry and updating an existing one use exactly the same syntax — assignment into a key. Python decides which one happens based on whether that key already exists:

Python
scores = {"Ana": 10}
scores["Ben"] = 8
scores["Ana"] = 12
print(scores)
Output
{'Ana': 12, 'Ben': 8}

scores["Ben"] = 8 created a brand-new entry because "Ben" wasn't in the dictionary yet. scores["Ana"] = 12 overwrote the existing value, replacing 10 with 12. A key can only appear once in a dictionary, so assigning to an existing key never produces a duplicate — it always replaces. Note also that "Ana" stayed in first position: updating a value doesn't move the entry.

To delete an entry, you have two options. The del statement removes it and gives you nothing back. The .pop() method removes it and hands you the value it just removed, which is handy when you need to use that value afterwards:

Python
inventory = {"bolts": 40, "nuts": 25, "washers": 60}
del inventory["washers"]
print(inventory)

removed = inventory.pop("nuts")
print(removed)
print(inventory)
print(inventory.pop("screws", 0))
Output
{'bolts': 40, 'nuts': 25}
25
{'bolts': 40}
0

.pop() accepts a default the same way .get() does, so inventory.pop("screws", 0) returned 0 rather than raising a KeyError for a part that was never in the inventory. Without that default, both del and .pop() raise a KeyError on a missing key, just like square brackets do.

Before reaching for a key, you can simply ask whether it's there. The in operator answers that question, and it's the most common guard you'll write around dictionary code:

Python
permissions = {"read": True, "write": False}
print("read" in permissions)
print("delete" in permissions)
print(False in permissions)
print(False in permissions.values())
Output
True
False
False
True

That third line trips up beginners: in on a dictionary searches the keys only, never the values. False in permissions is False because no key is named False, even though False is sitting right there as the value of "write". If you genuinely want to search the values, say so explicitly with .values(), as the last line does.

Looping Through a Dictionary

Looping over a dictionary directly gives you its keys, one at a time:

Python
prices = {"tea": 3, "coffee": 5, "juice": 4}
for name in prices:
    print(name)
Output
tea
coffee
juice

Three methods let you say precisely which part of each entry you want. .keys() gives the labels, .values() gives the stored data, and .items() gives both together as pairs:

Python
prices = {"tea": 3, "coffee": 5, "juice": 4}
print(list(prices.keys()))
print(list(prices.values()))
print(list(prices.items()))
Output
['tea', 'coffee', 'juice']
[3, 5, 4]
[('tea', 3), ('coffee', 5), ('juice', 4)]

Each of those methods returns a special view object rather than a list. A view is a live window onto the dictionary — it knows how to be looped over and how to answer in questions, but it isn't itself a list, which is why the examples above wrap each one in list() just to display it. In a for loop you never need that wrapper.

.items() is the one you'll use most, because it pairs naturally with unpacking two loop variables at once — the first receives the key, the second receives the value:

Python
prices = {"tea": 3, "coffee": 5, "juice": 4}
total = 0
for drink, price in prices.items():
    print(f"{drink} costs {price}")
    total += price
print("Total:", total)
Output
tea costs 3
coffee costs 5
juice costs 4
Total: 12

Because .values() produces just the numbers, built-in functions that work on sequences work on it directly — sum(), max(), and min() all accept it without any extra work:

Python
prices = {"tea": 3, "coffee": 5, "juice": 4}
print(sum(prices.values()))
print(max(prices.values()))
Output
12
5

What Can Legally Be a Key?

Values in a dictionary can be absolutely anything. Keys cannot — a key must be hashable.

Hashable means Python can run the value through a hash function: a calculation that turns the value into a fixed number, called its hash, which always comes out the same for the same value within a running program. Python uses that number to decide where in memory the entry gets filed, which is the trick that makes lookups fast (more on that in a moment). For that filing system to keep working, a key's hash must never change while it sits in the dictionary — and that means keys must be immutable, meaning they cannot be modified after they're created.

In practice, strings, integers, floats, booleans, and tuples of immutable things are all valid keys:

Python
locations = {}
locations["home"] = "warm"
locations[7] = "lucky number"
locations[(40.7, -74.0)] = "New York City"
print(locations)
print(locations[(40.7, -74.0)])
Output
{'home': 'warm', 7: 'lucky number', (40.7, -74.0): 'New York City'}
New York City

That tuple key is a genuinely useful pattern — a pair of coordinates, or a start and end city, is one logical label made of two pieces, and a tuple lets you use the whole pair as a single key. You can check hashability yourself with the built-in hash() function, which returns the same number for equal values:

Python
print(hash("python") == hash("python"))
print(hash((1, 2)) == hash((1, 2)))
Output
True
True

Lists, on the other hand, are mutable, so they're unhashable and rejected outright:

Python
try:
    grouped = {["a", "b"]: "value"}
except TypeError as error:
    print("TypeError:", error)
Output
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')

The reason isn't arbitrary. If a list could be a key, you could append to it after filing the entry, its hash would change, and Python would look in the wrong place and report that your own key had vanished. Forbidding it up front prevents that whole class of bug. The fix is to convert the list into a tuple, which holds the same values but can never change:

Python
routes = {("Paris", "Rome"): 1106}
key = tuple(["Paris", "Rome"])
print(routes[key])
Output
1106

Nesting Dictionaries

A dictionary value can be another dictionary. That's how you model something with structure — a record per person, where each record has its own named fields:

Python
students = {
    "ana": {"age": 21, "grades": [88, 92]},
    "ben": {"age": 22, "grades": [75, 80]},
}
print(students["ana"]["age"])
print(students["ben"]["grades"][1])
students["ana"]["grades"].append(95)
print(students["ana"])
Output
21
80
{'age': 21, 'grades': [88, 92, 95]}

Read those chained brackets strictly left to right. students["ana"] produces the inner dictionary {'age': 21, 'grades': [88, 92]}, and the next ["age"] then looks up a key in that result. The third line goes one step further: students["ben"]["grades"] produces a list, so [1] on the end is a list index, not a key. Mixing dictionaries and lists like this is normal, and it's exactly the shape data arrives in when a program reads settings files or web data.

The danger with nesting is that a single missing key anywhere in the chain raises a KeyError. Chaining .get() with an empty dictionary as the default keeps the chain alive when a record doesn't exist:

Python
students = {"ana": {"age": 21}}
print(students.get("cleo", {}).get("age", "unknown"))
Output
unknown

students.get("cleo", {}) found no "cleo", so it returned an empty dictionary. The second .get() call then runs on that empty dictionary, finds no "age" either, and returns its own fallback string. No crash, and a readable answer.

Why Dictionary Lookups Are Fast

Here's the property that makes dictionaries central to problem solving, not just to storing records.

To check whether a value is in a list, Python has no choice but to walk the list and compare items one by one. If the list holds ten thousand numbers and the one you want sits at the end, that's ten thousand comparisons. Double the list and you double the work.

A dictionary doesn't search at all. It runs your key through the hash function, gets a number, and uses that number to jump directly to the one place that entry could possibly be stored. This structure is called a hash table. The critical consequence: the work of a single lookup barely changes whether the dictionary holds ten entries or ten million. That's why in on a dictionary is described as taking roughly constant time, while in on a list takes time proportional to the list's length.

That difference unlocks a pattern you'll use constantly: instead of re-scanning data you've already looked at, remember it in a dictionary as you go, then ask one instant question per new item. Here's the pattern finding duplicates in a single pass:

Python
words = ["red", "blue", "red", "green", "blue", "red"]
seen = {}
duplicates = []
for word in words:
    if word in seen:
        duplicates.append(word)
    else:
        seen[word] = True
print(duplicates)
print(seen)
Output
['red', 'blue', 'red']
{'red': True, 'blue': True, 'green': True}

The loop touches each word exactly once. For every word it asks the seen dictionary a single instant question — have I met you before? — instead of rescanning the earlier part of the list. Note that "red" appears twice in duplicates because it showed up two extra times, while seen holds each distinct word only once, since assigning to an existing key just overwrites it.

This is precisely the idea behind the Two Number Sum practice problem linked at the end of this lesson. That problem hands you a list of integers and a target, and asks for the pair that adds up to the target. The slow approach compares every number against every other number. The fast approach scans the list once and, for each number, uses simple arithmetic to work out which partner value would complete the target — then asks a dictionary of already-seen numbers whether that partner has gone by already. Same shape as the loop above: one pass, one instant lookup per item.

A Worked Example

Let's combine .get() with a default, .items() iteration, and membership checking into one small program that counts how often each word appears in a sentence and reports the winner.

Python
def count_words(text):
    counts = {}
    for word in text.lower().split():
        cleaned = word.strip(".,!?")
        counts[cleaned] = counts.get(cleaned, 0) + 1
    return counts


def most_common(counts):
    best_word = ""
    best_count = 0
    for word, count in counts.items():
        if count > best_count:
            best_word, best_count = word, count
    return best_word, best_count


line = "The cat sat on the mat, and the cat slept."
tally = count_words(line)
print(tally)
word, count = most_common(tally)
print(f"Most common: '{word}' appears {count} times")
print("Is 'dog' in the text?", "dog" in tally)
print("Times 'mat' appears:", tally.get("mat", 0))
Output
{'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1, 'and': 1, 'slept': 1}
Most common: 'the' appears 3 times
Is 'dog' in the text? False
Times 'mat' appears: 1

Walking through it: count_words starts with counts = {}, an empty dictionary that will grow one key per distinct word. text.lower() makes the counting case-insensitive so "The" and "the" land on the same key, and .split() breaks the sentence into a list of words at the spaces.

word.strip(".,!?") removes any of those four punctuation characters from both ends of the word. Without it, "mat," and "mat" would be two different keys, since dictionary keys are compared exactly.

The single most important line is counts[cleaned] = counts.get(cleaned, 0) + 1. Read the right side first: counts.get(cleaned, 0) asks for the current count and returns 0 if this word has never been seen, so the very first sighting evaluates to 0 + 1. The result is then assigned back into counts[cleaned], which creates the entry on a first sighting and overwrites it on every sighting after. Writing counts[cleaned] + 1 instead would raise a KeyError on every new word — the default in .get() is what makes this one line work for both cases.

most_common walks the finished tally with .items(), unpacking each entry into word and count. It keeps a running champion in best_word and best_count, starting at 0 so the first real count always beats it, and replaces both together whenever it meets a higher count. Because "the" was counted three times and nothing beat it, that's what comes back.

The last two lines show the two read styles side by side. "dog" in tally is a membership test that returns a boolean and never errors. tally.get("mat", 0) is a value lookup with a safe default, which would have returned 0 just as calmly if "mat" had never appeared.

Common Mistakes

Using square brackets on a key that might not exist. This is the single most common dictionary bug. Square brackets assume the key is there, and raise a KeyError when it isn't:

Python
config = {"host": "localhost", "port": 8080}
try:
    print(config["timeout"])
except KeyError as error:
    print("Crashed with KeyError:", error)
Output
Crashed with KeyError: 'timeout'

The fix is .get() with a default that makes sense for your data, so a missing setting falls back to a reasonable value instead of stopping the program:

Python
config = {"host": "localhost", "port": 8080}
print(config.get("timeout", 30))
Output
30

Assuming .get() will raise an error when the key is missing. It won't — that's the whole point of it. It quietly returns None, and because None looks fine until you try to do arithmetic or string work with it, the real crash lands several lines later somewhere that looks unrelated:

Python
inventory = {"bolts": 40}
count = inventory.get("screws")
print(count)
try:
    print(count + 5)
except TypeError as error:
    print("TypeError:", error)
Output
None
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

The error message names NoneType, not the missing key, so it gives no hint about where the problem actually started. Always pass a default of the type you intend to work with — 0 for counters, "" for text, [] for collections:

Python
inventory = {"bolts": 40}
print(inventory.get("screws", 0) + 5)
Output
5

Trying to use a list as a key. Lists can change, so Python refuses to hash them, and the message you get names the type rather than explaining the rule:

Python
routes = {}
try:
    routes[["Paris", "Rome"]] = 1106
except TypeError as error:
    print("TypeError:", error)
Output
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')

Convert the list to a tuple, which stores the same values in the same order but cannot be modified and is therefore hashable:

Python
routes = {}
routes[("Paris", "Rome")] = 1106
print(routes)
print(routes[("Paris", "Rome")])
Output
{('Paris', 'Rome'): 1106}
1106

Looping over a dictionary and expecting values. A plain for loop over a dictionary yields keys, so code written as if it were handing you values fails on the first iteration:

Python
prices = {"tea": 3, "coffee": 5}
total = 0
try:
    for price in prices:
        total += price
except TypeError as error:
    print("TypeError:", error)
Output
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

The loop variable was named price, but it actually held the string "tea", and adding a string to an integer is not allowed. Ask for values explicitly with .values() — or for both parts with .items() when you need the key too:

Python
prices = {"tea": 3, "coffee": 5}
total = 0
for price in prices.values():
    total += price
print(total)
Output
8

Next Steps

Dictionaries turn "search through everything" into "ask one question," and that shift is what separates a slow solution from a fast one in most coding problems. The Two Number Sum practice problem is the natural next step: try the straightforward approach of comparing every pair first, then rewrite it using a dictionary of numbers you've already seen and notice how much work disappears.

Before that, open the Python playground and experiment with the examples here on your own data. Build a small dictionary, deliberately look up a key that doesn't exist with square brackets to see the KeyError for real, then do the same lookup with .get() and a default. Getting a feel for when each one is the right tool will make the practice problem much smoother.

When you want a harder one

Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.