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.
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[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
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.
| Term | Meaning | In the example |
|---|---|---|
| Key | The label you look a value up by | "name", "age", "city" |
| Value | The data stored under a key | "Alice", 28, "Seattle" |
| Pair | One key and its value together | "age": 28 |
| Dictionary | The whole collection of pairs | user |
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.
| Position | Value |
|---|---|
| 0 | Alice |
| 1 | 28 |
| 2 | Seattle |
| Key | Value |
|---|---|
| name | Alice |
| age | 28 |
| city | Seattle |
- 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
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.
{} 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.
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
What does this program print?
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:
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 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
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.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.
- 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
- 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)34# Add a NEW key-value pair5user["email"] = "alice@email.com"6print("After adding email:", user)78# Update an EXISTING value9user["age"] = 2910print("After updating age:", user)
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.
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.
- ▸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
What does the last line print?
"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.
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)34del user["city"]5print("After del:", user)
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"].
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"}23# Remove and get the value4city = user.pop("city")5print("Removed city:", city)6print("Remaining:", user)78# Safe pop with default (key doesn't exist)9country = user.pop("country", "Unknown")10print("Country:", country)
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.
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.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.
| Operation | Hands back | Key missing |
|---|---|---|
| del d[key] | Nothing | KeyError |
| d.pop(key) | The removed value | KeyError |
| d.pop(key, default) | The removed value, or the default | Returns the default, no error |
| d.clear() | Nothing | No key involved, never errors |
- 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 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
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.
What does the print statement show?
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
"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.
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.
- 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
- 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
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".
| Type | Allowed as a key | Example |
|---|---|---|
| str | Yes | "name" |
| int | Yes | 404 |
| bool | Yes | True |
| list | No, TypeError | ["a", "b"] |
| dict | No, TypeError | {"a": 1} |
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
What does this program print?
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:
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.
Most real programs use dictionaries for 2 jobs: holding settings and counting things.
Settings with sensible defaults
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.
| Setting | In config | Default | Value used |
|---|---|---|---|
| theme | yes | light | dark |
| font_size | yes | 12 | 14 |
| language | no | en | en |
Counting with a dictionary
1# Count word frequencies2word_counts = {}3words = ["apple", "banana", "apple", "cherry", "apple", "banana"]45for word in words:6 if word in word_counts:7 word_counts[word] = word_counts[word] + 18 else:9 word_counts[word] = 11011print(word_counts)
| Word | Already a key | Action | Dictionary after |
|---|---|---|---|
| apple | no | create apple: 1 | {'apple': 1} |
| banana | no | create banana: 1 | {'apple': 1, 'banana': 1} |
| apple | yes | 1 + 1 = 2 | {'apple': 2, 'banana': 1} |
| cherry | no | create cherry: 1 | {'apple': 2, 'banana': 1, 'cherry': 1} |
| apple | yes | 2 + 1 = 3 | {'apple': 3, 'banana': 1, 'cherry': 1} |
| banana | yes | 1 + 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.
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
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
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
What does the final line print?
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:
"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.
> 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.
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
- 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
- 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
- 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:
- 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
- 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