BeginnerPython · 34 min · 23 challenges

Dictionaries: Beginner

If Spotify stored each track as a list running title, artist, tempo, duration, and someone inserted an album field at position 1, every job reading track[2] for tempo would get the artist instead. Playlists would sort by artist name. A dictionary asks for track["tempo"] by name, so no insertion can shift it.

Creating Your First Dictionary

Daily Life
Interviews

A list stores items by position. That works when the position means something, such as the order of names in a queue, and fails when it does not. Here is a single person stored as a list:

user = ["Alice", 28, "Seattle"]
print(user[0])
print(user[2])
>>>Output
Alice
Seattle

user[0] is the name and user[2] is the city because whoever built the list put them there. Nothing in the code says so, and a reader has to count positions to find out. Insert a phone number at position 1 and every user[2] in the program silently returns the age instead of the city. A dictionary removes the counting: each value is stored under a key that you choose, and you ask for the value by that key.

The shape of a dictionary

user = {
"name": "Alice",
"age": 28,
"city": "Seattle",
}

Curly braces open and close the dictionary. Inside, each entry puts a colon between its key and its value, and commas separate the entries. The trailing comma after the last pair is optional, and the whole thing fits on a single line when it is short: {"name": "Alice", "age": 28}. Python programmers call this type a dict, and the word appears in error messages and documentation.

TermMeaningIn the example
KeyThe label you look a value up by"name", "age", "city"
ValueThe data stored under a key"Alice", 28, "Seattle"
PairOne key and its value together"age": 28
DictionaryThe whole collection of pairsuser
user = {"name": "Alice", "age": 28, "city": "Seattle"}
print(user)
print(user["name"])
print(user["city"])
>>>Output
{'name': 'Alice', 'age': 28, 'city': 'Seattle'}
Alice
Seattle

Python prints the dictionary in the order the pairs were written, with single quotes around strings. The lookup user["name"] uses square brackets the way a list does, but what goes between them is a key rather than a position. "name" means the same thing on every line of the program, no matter how many pairs come before it.

As a list
PositionValue
0Alice
128
2Seattle
As a dictionary
KeyValue
nameAlice
age28
citySeattle
Both tables hold the same 3 values. In the left table the labels live in your head. In the right table they live in the data, so a program can read and check them.
Reach for a dictionary when
  • Each value answers a question with a name, such as what is this user's city
  • Values may be added later and position would shift
  • A reader should understand a lookup without counting

Keys are unique, values are free

prices = {"apple": 120, "pear": 90, "apple": 150}
print(prices)
print(len(prices))
>>>Output
{'apple': 150, 'pear': 90}
2

A key appears once in a dictionary. Writing "apple" twice in the literal raises no error: the second value overwrites the first, so the dictionary ends up with 2 pairs and prices["apple"] is 150. len() counts pairs the same way it counts items in a list.

empty = {}
print(empty)
stock = {"apple": 0, "pear": 0, "plum": 12}
mixed = {"name": "Bo", "tags": ["admin", "beta"], "active": True}
print(stock)
print(mixed["tags"])
>>>Output
{}
{'apple': 0, 'pear': 0, 'plum': 12}
['admin', 'beta']

{} with nothing inside is an empty dictionary, and most real dictionaries start that way and fill up as a program runs. A value can be anything Python can store, even a whole list, as "tags" shows. Values may repeat freely, which is why both "apple" and "pear" hold 0 at the same time, while keys cannot repeat.

TIP
Name a dictionary after what it maps, the way prices and stock are named; user follows the same pattern. Name each key for what its value means. d["x"] tells a reader nothing; product["weight_kg"] tells them everything they need.

Practice

Multiple Choice

What does this program print?

pet = {"name": "Rex", "age": 4, "name": "Max"}
print(pet["name"], pet["age"])

Writing a literal from scratch takes the same 3 pieces of punctuation for every pair: quotes around a string key, a colon after it, and a comma before the next pair. The value follows the colon in whatever form it needs, so text gets quotes and a number does not: 40 and "40" are different values. Here is a desk lamp written one pair per line, the layout most code uses once a dictionary grows past 2 or 3 pairs:

lamp = {
"name": "Desk Lamp",
"watts": 40,
"color": "black",
}
print(lamp["watts"])
print(len(lamp))
print(lamp)
>>>Output
40
3
{'name': 'Desk Lamp', 'watts': 40, 'color': 'black'}

The layout changes nothing about the dictionary: Python ignores line breaks inside the braces, so this lamp matches the one-line version pair for pair. Putting a pair on each line makes a missing colon or comma stand out, because every line inside the braces ends the same way.

Reading and Updating Values

Daily Life
Interviews

Reading a value is the lookup from the previous section: square brackets with the key inside. That lookup fails when the key is missing, and adding a new key uses the same brackets on the left of an assignment.

Square brackets demand that the key exist

user = {"name": "Alice", "age": 28}
print(user["name"])
print(user["email"])

The first print runs and shows Alice. The second asks for "email", which the dictionary doesn't hold, and Python stops the program with a KeyError. The last line of the traceback names the missing key in quotes, which is the first place to look when you see one. No print after line 3 would run.

.get() asks politely

user = {"name": "Alice", "age": 28}
print(user.get("name", "unknown"))
print(user.get("email", "no email on file"))
print(user.get("age", 0) + 1)
>>>Output
Alice
no email on file
29

user.get(key, default) looks the key up and returns its value when the key exists. When it does not, it returns the default you passed instead of stopping the program. The third line shows that the returned value is ordinary data: user.get("age", 0) is 28 here, so adding 1 prints 29. You can leave the default off and write user.get("email"), in which case a missing key returns a special value called None that stands for no value; a later lesson covers None, and until then passing a default keeps every result predictable.

.get never adds anything. After user.get("email", "none") the dictionary still has 2 pairs, the same as before the call.

✓d[key]
  • Returns the value when the key exists
  • Stops the program with KeyError when it does not
  • Use it when a missing key is a bug you want to notice
✗d.get(key, default)
  • Returns the value when the key exists
  • Returns the default when it does not
  • Use it when a missing key is a normal case, such as an optional setting

Writing with square brackets

Assignment through square brackets has the same shape as reading, with the lookup on the left of =. If the key is new, Python adds the pair; if the key already exists, Python replaces its value. Step through this program and watch the dictionary change:

1user = {"name": "Alice", "age": 28}
2print("Before:", user)
3
4# Add a NEW key-value pair
5user["email"] = "alice@email.com"
6print("After adding email:", user)
7
8# Update an EXISTING value
9user["age"] = 29
10print("After updating age:", user)
user
:
:
:
output0 lines
user is a new dict that holds 2 keys
1/7

After line 5 the dictionary has 3 pairs, and after line 9 it still has 3: "age" kept its single pair while its value went from 28 to 29. Brackets behave differently for reads and writes, because reading a missing key crashes and writing to a missing key creates it. Every = with brackets on the left either adds or replaces, and never raises a KeyError.

user = {"name": "Alice", "tags": ["beta"]}
user["tags"].append("admin")
user["name"] = "Alicia"
print(user)
>>>Output
{'name': 'Alicia', 'tags': ['beta', 'admin']}

When a value is a list, user["tags"] hands you that list, and .append changes it in place, so the dictionary sees the new tag without any assignment back. Replacing "name" works as before. Reading and writing combine in 1 line: stock["apple"] = stock["apple"] + 2 adds 2 to the old count read on the right and stores the sum under the same key.

3 operations with the same brackets
  • ▸user["age"] reads: KeyError if the key is missing
  • ▸user["age"] = 29 writes: adds or replaces, never a KeyError
  • ▸user.get("age", 0) reads with a fallback: never a KeyError, never adds

Practice

Multiple Choice

What does the last line print?

stock = {"apple": 3}
stock["pear"] = 5
stock["apple"] = stock["apple"] + 2
print(stock.get("plum", 0), stock["apple"], len(stock))
One more worked case before you write your own. An update line runs its right side first: Python evaluates the right side, lookup and arithmetic included, and only then stores the result under the key on the left. A new key needs no such read, because there is nothing to read yet.
temps = {"mon": 18, "tue": 21}
temps["wed"] = 19
temps["mon"] = temps["mon"] + 4
print(temps)
print(temps.get("thu", 0))
print(len(temps))
>>>Output
{'mon': 22, 'tue': 21, 'wed': 19}
0
3

"wed" joins at the end through a plain assignment, and "mon" keeps its first position while its value goes from 18 to 22. The .get for "thu" returns 0 and leaves the dictionary at 3 pairs. Writing temps["thu"] + 4 on the right of an assignment would have stopped the program with a KeyError, since a missing key can't be read with brackets.

Removing Items

Daily Life
Interviews

Most dictionaries also lose pairs, the way a cart empties or a session ends. Python gives 3 ways to take pairs out, and they differ in what they hand back and in what they do when the key is already gone.

del removes a pair

1user = {"name": "Alice", "age": 28, "city": "Seattle"}
2print("Before:", user)
3
4del user["city"]
5print("After del:", user)
user
:
:
:
output0 lines
user is a new dict that holds 3 keys
1/5

del user["city"] removes both the key and its value; the dictionary shrinks from 3 pairs to 2 and "city" is gone as if it had never been written. del is a statement rather than a function, so it hands nothing back: there is no way to write x = del user["city"].

user = {"name": "Alice", "age": 28}
del user["age"]
print(user)
del user["age"]

Deleting a key that is absent raises the same KeyError as reading one. The first del succeeds, the print shows the 1 remaining pair, and the second del stops the program. When a program might delete the same key twice, del on its own is unsafe.

pop removes and returns

1user = {"name": "Alice", "age": 28, "city": "Seattle"}
2
3# Remove and get the value
4city = user.pop("city")
5print("Removed city:", city)
6print("Remaining:", user)
7
8# Safe pop with default (key doesn't exist)
9country = user.pop("country", "Unknown")
10print("Country:", country)
city=
country=
user
:
:
:
output0 lines
user is a new dict that holds 3 keys
1/7

user.pop("city") does 2 things: it removes the pair and it returns the value, so city holds "Seattle" after the call. That makes pop the tool when you still need the value, such as moving an item from a cart to an order. The second call shows the safe form: user.pop("country", "Unknown") finds no "country" key, leaves the dictionary alone, and returns the default.

scores = {"ana": 91, "ben": 78}
removed = scores.pop("ben")
print(removed)
print(scores)
print(scores.pop("cara", 0))
print(scores.pop("cara"))

pop with 1 argument behaves like del: a missing key is a KeyError. pop with 2 arguments never raises for a missing key, because the second argument is what to return when there's nothing to remove. The traceback here comes from the final line only; the 3 prints before it all ran and their output stays on screen.

clear empties everything

cart = {"apple": 3, "pear": 1}
cart.clear()
print(cart)
print(len(cart))
>>>Output
{}
0

cart.clear() removes every pair at once, leaving the dictionary with 0 pairs. The name cart still refers to it, and new pairs can be added afterwards with bracket assignment. Calling clear() on an already empty dictionary does nothing and raises nothing.

OperationHands backKey missing
del d[key]NothingKeyError
d.pop(key)The removed valueKeyError
d.pop(key, default)The removed value, or the defaultReturns the default, no error
d.clear()NothingNo key involved, never errors
✓Do
  • Use pop with a default when a key may already be gone
  • Use pop when you need the value you are removing
  • Use del when the key must exist and a crash should flag the bug
✗Don't
  • Don't call del twice on the same key without a check between
  • Do not use pop(key) without a default for optional keys
  • Do not remove pairs one at a time to empty a dictionary; use clear

Practice

Real programs mix the removal tools, and the order of the calls decides what each one finds. Trace this session cleanup line by line before you run it:
session = {"user": "ana", "token": "x91", "cart": 2}
token = session.pop("token")
del session["cart"]
print(token)
print(session.pop("cart", "already gone"))
print(session)
>>>Output
x91
already gone
{'user': 'ana'}

Line 2 takes "token" out and keeps its value in token. Line 3 removes "cart", so by line 5 the key is absent and pop hands back its default instead of a count. Change line 5 to session.pop("cart") and the program would print x91, then stop with KeyError: 'cart'. Only "user" is left at the end.

Multiple Choice

What does the print statement show?

cart = {"apple": 2, "pear": 1, "plum": 4}
taken = cart.pop("pear")
del cart["plum"]
print(taken, cart.pop("plum", "gone"), cart)

Checking for Keys and Key Types

Daily Life
Interviews

Giving .get a default handles a missing key by substituting a value. Sometimes you want to ask the question directly: is this key here? Python answers with the in operator, and an if uses the answer to pick which branch runs.

in and not in

user = {"name": "Alice", "age": 28}
print("name" in user)
print("email" in user)
print("email" not in user)
print("Alice" in user)
>>>Output
True
False
True
False

"name" in user is True because "name" is a key. "email" in user is False, and not in flips the answer. The fourth line is the one people get wrong: "Alice" in user is False even though "Alice" is sitting right there in the dictionary. in looks at keys only. Values are reached through their keys, never searched by in.

settings = {"theme": "dark"}
if "theme" in settings:
print("theme is", settings["theme"])
if "font" in settings:
print("font is", settings["font"])
else:
print("no font set")
>>>Output
theme is dark
no font set

This is the pattern: test with in, then read with brackets inside the branch where the key is known to exist. The bracket read on line 3 can't raise, because line 2 already confirmed the key. The else branch handles the absent case with whatever makes sense, here a message. Compared with .get, which folds the test and the fallback into a single expression, in lets you run different code on each side.

✓Use in when
  • The absent case needs its own code, not a stand-in value
  • A loop must decide between creating and updating an entry
  • You want to confirm a key before del or a bracket read
✗Use .get when
  • A default value is all the absent case needs
  • The lookup sits inside a larger expression
  • One line beats an if/else

What can be a key

lookup = {"a": 1, 7: "seven", True: "yes"}
print(lookup["a"])
print(lookup[7])
print(lookup[True])
>>>Output
1
seven
yes
Strings are the most common keys. Integers and booleans work as well, and a single dictionary can mix them. Python finds a key by turning it into a number called a hash, and uses that number to choose where the pair is stored. For that to work, a key must never change after it goes in: if it changed, its hash would change, Python would look in the wrong place, and the pair would be lost. None of these 3 types can be changed once created, so all of them qualify.
bad = {["x", "y"]: 1}

A list can change, so Python refuses it as a key before the dictionary is even built. The error is a TypeError with the word unhashable, which is Python's way of saying the object has no stable hash. The fix depends on what you meant; usually the list belongs on the value side, under a string key such as "coords".

TypeAllowed as a keyExample
strYes"name"
intYes404
boolYesTrue
listNo, TypeError["a", "b"]
dictNo, TypeError{"a": 1}
codes = {1: "one", "1": "string one"}
print(codes[1])
print(codes["1"])
print(len(codes))
>>>Output
one
string one
2

The integer 1 and the string "1" are different keys, because they're different values: 1 == "1" is False. A dictionary filled from typed input holds "42", because everything typed arrives as text, so a lookup with the number 42 will miss. Mixing key types is allowed, as the lookup example showed, but when keys come from input, settle on either text or numbers for that dictionary's keys so every lookup uses the type the keys were stored with.

Practice

Multiple Choice

What does this program print?

ages = {"ana": 31, "ben": 25}
print("ana" in ages, 31 in ages, "cara" not in ages)

The in test also guards a removal. The Removing Items section showed that a second del on the same key crashes; checking first lets the program take whichever branch fits:

settings = {"theme": "dark", "font": "mono"}
if "font" in settings:
del settings["font"]
if "font" in settings:
del settings["font"]
else:
print("font already removed")
print(settings)
>>>Output
font already removed
{'theme': 'dark'}

The first if finds "font" and deletes it. The second gets False from the same question and runs the else branch, so the second del never executes. The test and the action name the same key: a check for "font" protects a del or bracket read of "font" and nothing else.

Real-World Use Cases

Daily Life
Interviews

Most real programs use dictionaries for 2 jobs: holding settings and counting things.

Settings with sensible defaults

config = {"theme": "dark", "font_size": 14}
theme = config.get("theme", "light")
font = config.get("font_size", 12)
lang = config.get("language", "en")
print(theme, font, lang)
>>>Output
dark 14 en

A config dictionary holds the settings a user or a deployment changed. Anything they left alone is absent, and .get with a default fills it in: "language" was never set, so the program runs in "en". The defaults live in the code at the point of use, the overrides live in the dictionary, and neither side needs to know which keys the other has.

SettingIn configDefaultValue used
themeyeslightdark
font_sizeyes1214
languagenoenen

Counting with a dictionary

To count how often each word appears in a list, you need 1 counter per distinct word, and you do not know the words in advance. A dictionary with the word as key and the count as value grows a counter the first time each word shows up:
1# Count word frequencies
2word_counts = {}
3words = ["apple", "banana", "apple", "cherry", "apple", "banana"]
4
5for word in words:
6 if word in word_counts:
7 word_counts[word] = word_counts[word] + 1
8 else:
9 word_counts[word] = 1
10
11print(word_counts)
word=
word_counts
:
:
:
words
[
]
012345
output0 lines
word_counts is a new dict that starts empty
1/23
WordAlready a keyActionDictionary after
applenocreate apple: 1{'apple': 1}
banananocreate banana: 1{'apple': 1, 'banana': 1}
appleyes1 + 1 = 2{'apple': 2, 'banana': 1}
cherrynocreate cherry: 1{'apple': 2, 'banana': 1, 'cherry': 1}
appleyes2 + 1 = 3{'apple': 3, 'banana': 1, 'cherry': 1}
bananayes1 + 1 = 2{'apple': 3, 'banana': 2, 'cherry': 1}

Depending on the membership test, each word gets one of 2 bracket writes: create the pair with a value of 1, or replace the current count with that count plus 1. After 6 words the dictionary holds 3 pairs, apple at 3, banana at 2, cherry at 1, in the order each word first appeared. The same loop counts anything that can be a key: status codes, letter grades, product ids.

scores = [92, 67, 85, 74, 59, 88]
grades = {}
for score in scores:
if score > 84:
grade = "A"
elif score > 69:
grade = "B"
else:
grade = "C"
if grade in grades:
grades[grade] = grades[grade] + 1
else:
grades[grade] = 1
print(grades)
>>>Output
{'A': 3, 'C': 2, 'B': 1}

Here the key is computed before the count. Each score becomes a letter through the if/elif/else chain, and the tally code underneath never changes. "A" enters the dictionary first because 92 is the first score, then "C" for 67, then "B" for 74, so that is the printed order even though B sits between A and C alphabetically.

Lookup tables

status_text = {200: "OK", 404: "Not Found", 500: "Server Error"}
codes = [200, 404, 418, 200]
for code in codes:
print(code, status_text.get(code, "Unknown"))
>>>Output
200 OK
404 Not Found
418 Unknown
200 OK

A dictionary that translates one value into another is a lookup table. Code 418 has no entry, so .get supplies "Unknown" and the loop keeps going; a bracket lookup would have crashed on the third code. Lookup tables replace long if/elif chains: supporting a new status means adding 1 pair, with no new branch to write or test.

Capstone: a tiny inventory

This program starts from a stocked dictionary and runs each order through a membership test before touching the counts with bracket reads and writes. One removal comes at the end. Predict every printed line before you reveal the output.
stock = {"apple": 4, "pear": 0, "plum": 7}
orders = ["apple", "fig", "pear", "apple"]
for item in orders:
if item not in stock:
print(item, "not stocked")
elif stock[item] == 0:
print(item, "sold out")
else:
stock[item] = stock[item] - 1
print(item, "shipped, left:", stock[item])
del stock["pear"]
print(stock)
>>>Output
apple shipped, left: 3
fig not stocked
pear sold out
apple shipped, left: 2
{'apple': 2, 'plum': 7}

Order by order: "fig" fails the not in test and prints not stocked, with no bracket read ever attempted on it. "pear" is a key but its count is 0, so the elif catches it. Each "apple" order writes back the count minus 1, so the count goes from 4 to 3 to 2. The final del drops the sold-out pear, and the printed dictionary shows the 2 remaining products with their updated counts.

Practice

Multiple Choice

What does the final line print?

votes = ["red", "blue", "red", "green", "red"]
tally = {}
for color in votes:
if color in tally:
tally[color] = tally[color] + 1
else:
tally[color] = 1
print(tally["red"], tally.get("blue", 0), tally.get("pink", 0))

Two dictionaries often work as a pair: one counts what happened, the other holds settings that judge the counts. Here a limits config caps how many of each item an order may hold, and any item without its own entry gets a default limit of 1:

counts = {"tea": 4, "jam": 1}
limits = {"tea": 3}
for item in ["tea", "jam"]:
limit = limits.get(item, 1)
if counts[item] > limit:
print(item, "over limit:", counts[item], "of", limit)
else:
print(item, "within limit:", counts[item], "of", limit)
>>>Output
tea over limit: 4 of 3
jam within limit: 1 of 1

"tea" is over its own limit of 3 with a count of 4. "jam" has no entry in limits, so .get supplies 1, and a count of 1 is not greater than 1. The counts are read with brackets because the loop only asks for items it knows were counted; the limits are read with .get because most items have no entry of their own.

❯❯❯PUTTING IT ALL TOGETHER

> A Shopify merchant's storefront loads its settings and tracks sales during a flash sale. The store's config dictionary holds the handful of settings the merchant changed, such as the theme and currency, and the checkout code reads every setting with .get and a default, so a merchant who never touched the language setting still gets English. Each incoming order carries a product id, and a counting loop keeps a dictionary of units sold per product: a membership test decides whether to create the entry at 1 or read the count and add 1. When a product sells out, its entry is popped with a default so that a duplicate sold-out signal cannot crash the sale, and the returned count is what the dashboard shows as the final total.

The config dictionary is the settings pattern from Section 4, built with the literal syntax from Section 0 and read with .get from Section 1.
The units-sold loop is the counting pattern, which rests on the in test from Section 3 choosing between the 2 bracket writes from Section 1.
Popping a sold-out product with a default is the safe removal from Section 2, chosen because the returned count is still needed.
Product ids are strings everywhere in the system, which keeps the key type consistent so that lookups from the order feed and the dashboard find the same pairs.
KEY TAKEAWAYS
A dictionary stores values under keys you choose, so a lookup reads by name instead of by counted position.
d[key] reads and crashes with KeyError when the key is absent; d.get(key, default) reads and returns the default instead.
d[key] = value adds the pair when the key is new and replaces the value when it exists, and never raises a KeyError.
del removes a pair and returns nothing, while pop also returns the value. Given a default, pop is the only way to remove one key that never crashes when the key is already gone.
in and not in test keys only, never values, and a membership test before a bracket read makes the read safe.
Keys must be values that can't change. Strings and integers qualify, as do booleans, while lists and dictionaries raise TypeError.
Configs read with .get and counting loops with in cover most of what dictionaries do in everyday programs.

Store data under names you choose, then find, change and remove it by that name instead of by position.

Category
Python
Difficulty
beginner
Duration
34 minutes
Challenges
23 hands-on challenges

Topics covered: Creating Your First Dictionary, Reading and Updating Values, Removing Items, Checking for Keys and Key Types, Real-World Use Cases

Lesson Sections

  1. Creating Your First Dictionary (concepts: Dictionaries)

    The shape of a dictionary Both tables hold the same 3 values. In the left table the labels live in your head. In the right table they live in the data, so a program can read and check them. Keys are unique, values are free Practice

  2. Reading and Updating Values (concepts: Dict Methods)

    Square brackets demand that the key exist .get() asks politely .get never adds anything. After user.get("email", "none") the dictionary still has 2 pairs, the same as before the call. Writing with square brackets Practice One more worked case before you write your own. An update line runs its right side first: Python evaluates the right side, lookup and arithmetic included, and only then stores the result under the key on the left. A new key needs no such read, because there is nothing to read y

  3. Removing Items (concepts: Dict Methods)

    del removes a pair pop removes and returns clear empties everything Practice Real programs mix the removal tools, and the order of the calls decides what each one finds. Trace this session cleanup line by line before you run it:

  4. Checking for Keys and Key Types (concepts: Dictionaries)

    in and not in What can be a key Strings are the most common keys. Integers and booleans work as well, and a single dictionary can mix them. Python finds a key by turning it into a number called a hash, and uses that number to choose where the pair is stored. For that to work, a key must never change after it goes in: if it changed, its hash would change, Python would look in the wrong place, and the pair would be lost. None of these 3 types can be changed once created, so all of them qualify. Pr

  5. Real-World Use Cases (concepts: Frequency Counting)

    Settings with sensible defaults Counting with a dictionary To count how often each word appears in a list, you need 1 counter per distinct word, and you do not know the words in advance. A dictionary with the word as key and the count as value grows a counter the first time each word shows up: Lookup tables Capstone: a tiny inventory This program starts from a stocked dictionary and runs each order through a membership test before touching the counts with bracket reads and writes. One removal co