A dictionary stores pairs of values: a key you use to find something, and the value the key points to. Looking up a product by its name, finding a customer by their email, fetching the price of an item by its product code, all of these are dictionary jobs. This lesson covers what a dictionary actually is, the three common ways to create one, how to read values by key, and the rule about which kinds of things can be used as keys.
A dictionary is a mutable collection of key-value pairs, where each key maps to one value. The classic mental model is a phone book: you don't search the whole book to find a number, you look up the name and the number is right next to it. A Python dictionary works the same way. You give it a key, it gives you the value, in roughly the time it takes to read one slot.
Three pairs, written as key: value with commas between them and curly braces around the whole thing. The key on the left, the value on the right, separated by a colon. When you write product_prices["Wireless Mouse"], Python finds the matching key and hands you back the value next to it.
The same kind of lookup with a list would force you to walk the items until you found the one you wanted. With a dictionary, the lookup is direct.
The cyan boxes are the keys, the orange boxes are the values, and each arrow is one pair. You can think of the dictionary as a small table with two columns. The keys column is what you search by, the values column is what you get back.
A dictionary is the right tool any time you have a "look up X by Y" relationship. Product to price, customer ID to customer record, order number to order status, country code to country name. If the question your code keeps asking is "given this key, what's the value?", a dictionary is almost always the answer.
There are three common ways to make a dictionary. You'll write the first one most of the time and reach for the other two in specific situations.
{}A dictionary literal is curly braces around a comma-separated list of key: value pairs. This is what you'll write in everyday code.
{} is an empty dictionary, ready to have pairs added later. One small gotcha: empty curly braces are a dict, not a set. An empty set is written set(), not {}, because the braces were claimed by dictionaries first.
The trailing comma on the last pair is allowed and conventional for multi-line literals:
The trailing comma is the same convenience as with lists: future edits are cleaner because adding a new line doesn't require remembering to add a comma to the previous one.
The values in a dictionary can be any type at all: strings, numbers, booleans, lists, even other dictionaries. Mixing types within one dictionary is normal and useful. The keys are stricter, and we'll get to that rule in a moment.
Four pairs, four different value types. The items value is a list, which is fine; values can be any object. This kind of mixed-value dictionary is how you'd describe one order record before reaching for a dataclass or a class.
dict() ConstructorThe second way is dict(), the constructor. With no arguments, it's the same as {}:
Most Python developers prefer {} because it's shorter and reads as "an empty dict" at a glance. dict() earns its keep when you have data in another shape that you want to turn into a dictionary, like a list of pairs:
Each two-item tuple becomes one key-value pair. This is exactly the shape you get back from things like enumerate, zip, or rows from a CSV file, so dict(...) is the bridge between sequence-shaped data and dictionary-shaped data.
zip(products, prices) pairs each product with its price, and dict(...) turns those pairs into a dictionary. This pattern shows up constantly when you have two parallel lists and want to look up one by the other.
dict(key=value)The third way is also the constructor, but called with keyword arguments: dict(name=value, name=value, ...). Every keyword name becomes a key in the resulting dictionary, and every keyword value becomes its value.
This is a shorter, often nicer-looking way to build a dictionary when the keys are all simple strings that happen to be valid Python identifiers. You don't have to put quotes around the keys, and the result is the same dict you'd get from {"name": "Alice", ...}.
The catch is in that phrase "valid Python identifiers". Keyword argument names must be plain identifiers: letters, digits, underscores, no leading digit, no spaces, no reserved words. That means this form can't build dictionaries with keys like "first name", "123", or "class". For those, you need the literal {} syntax or a list of pairs.
Pick the literal {} form for everyday dictionary creation. Reach for dict(pairs) or dict(zip(...)) when you have pair-shaped or two-list-shaped data. Use dict(key=value) when every key is a plain identifier and the keyword style reads cleaner than quoting.
[]The headline operation on a dictionary is lookup by key. The bracket syntax prices["Wireless Mouse"] finds the value that's mapped to that key and returns it.
The syntax looks like list indexing (cart[0]), but it works differently underneath. With a list, the number in the brackets is a position. With a dictionary, the value in the brackets is a key, and Python uses that key's hash to jump directly to where the value is stored.
The lookup cost is what makes dictionaries special. Whether the dictionary has 10 pairs or 10 million, looking up one key takes roughly the same amount of time. This is the property worth remembering above all others.
Cost: Dictionary lookup by key is O(1) on average. The dictionary hashes the key and uses that hash to jump straight to the right slot, without scanning the other pairs. The worst case is O(n) if many keys hash to the same slot, but that's rare with normal use.
The contrast with a list is what makes dictionaries useful for lookup. With a list of (product, price) pairs, finding the price of "Wireless Mouse" means walking the list until you find a matching name, which is an O(n) operation. With a dictionary keyed on product name, the same lookup is O(1).
Same answer, much less work in the dictionary version, and the gap grows as the data gets bigger. A thousand-product catalog with a price lookup is barely slower in the dict version. The list version does a thousand times more work in the worst case.
Cost: name in some_list and some_list.index(name) are O(n). If you find yourself doing membership checks or lookups against the same list over and over, build a dict (or a set) once and check against it instead.
Asking for a key that isn't in the dictionary raises KeyError. Python doesn't return None, doesn't return an empty string, doesn't silently invent the key. It tells you loudly:
Loud failure is the right behavior. A silent default would let bugs hide, and "the value at this key is missing" is almost never the same situation as "the value at this key is None". KeyError tells you the line that's wrong, and the key that wasn't found.
If you actually want a default for missing keys, there's a method called get that handles that. For now, the rule is simple: bracket lookup assumes the key exists, and raises if it doesn't.
A dictionary can be changed after it's created. Adding a new key, replacing the value at an existing key, both happen with the same assignment syntax: prices[key] = value.
The first assignment adds "HDMI Cable" as a new key with value 14.99. The second assignment finds the existing "Wireless Mouse" key and replaces its value with 24.99. Same syntax, different effect, depending on whether the key already exists.
This is the big behavior split between list indexing and dict indexing. With a list, cart[5] = "Webcam" fails if there's no slot 5; index assignment only replaces existing slots. With a dict, prices["Webcam"] = 89.99 works whether or not "Webcam" is already a key; assignment creates the key if it's missing.
The dictionary itself is the same object before and after. Only its contents change. If two variables point to the same dictionary, both see the update:
alias = prices doesn't copy the dictionary, it just binds another name to the same one. Adding a key through alias is visible through prices. This is the same reference behavior as with lists, and the same caveat applies: if you want an independent copy, you need to copy explicitly.
Cost: Adding a key and replacing a value are both O(1) on average, the same as lookup. Python computes the key's hash, finds the right slot, and writes the value. Growing the dictionary occasionally triggers an internal resize, but that cost averages out to O(1) per operation across many inserts.
Assignment is the simplest way to grow a dictionary, but it's not the only one. Other tools like update and the merge operator | offer additional ways to add or change pairs.
Not anything can be a dictionary key. Python requires keys to be hashable, which is the property that lets the dictionary jump directly to a slot instead of scanning.
A hashable object has two requirements: it has a __hash__ method that returns a stable integer, and its value doesn't change over its lifetime. The second part is the important one for everyday use, because it's the reason mutable objects can't be keys. If you used a list as a key and then modified the list, the dictionary would look in the wrong slot the next time you tried to find it.
In practice, the rule comes down to a short list:
| Type | Hashable? | Example as Key |
|---|---|---|
str | Yes | "name" |
int | Yes | 42 |
float | Yes | 3.14 |
bool | Yes | True |
tuple of hashables | Yes | ("US", "NYC") |
frozenset | Yes | frozenset({"a", "b"}) |
list | No | [1, 2] raises TypeError |
dict | No | {"a": 1} raises TypeError |
set | No | {1, 2} raises TypeError |
tuple containing a list | No | (1, [2]) raises TypeError |
Strings are by far the most common key type. They're immutable, hashable, and human-readable, which makes them the natural fit for product names, customer emails, order IDs, and almost any other kind of label. Integers are the second most common; you'll see them used for things like a customer ID or a product code.
Tuple keys are worth a second look. Because a tuple of hashable values is itself hashable, you can use a tuple to represent a composite key: (country, city), (category, brand), (year, month). This is one of the small features that makes Python pleasant for data work.
A tuple stops being hashable the moment it contains an unhashable value. (1, 2, 3) is hashable, but (1, [2, 3]) is not, because the list inside it is mutable.
The error says unhashable type: 'list' because the list is the part inside the tuple that broke the rule. If you needed that exact composite key, you'd convert the list to a tuple first: ("US", ("NYC", "Boston")).
Lists themselves are the most common "I tried to use this as a key" mistake. Python rejects them outright:
The fix depends on what you were trying to do. If you wanted a fixed group of items as one key, use a tuple: prefs[("Mouse", "Cable")] = "favorites". If you wanted to use the list's contents as a flat key, you probably wanted a different data shape entirely (maybe a set of keys, each mapping to the same value).
You've now seen three different ways to hold a collection. The question worth answering before reaching for any of them is which one fits the problem.
| Question | Use a... |
|---|---|
| Do I need to look things up by a label or ID? | dict |
| Is the order of items what I care about? | list |
| Is this a fixed, small record where each position has a known meaning? | tuple (or named tuple) |
| Will the collection grow and shrink at runtime? | list or dict |
| Is the collection a "bag of items I'll iterate over"? | list |
Do I need fast membership checks (x in collection)? | dict (or set) |
A few concrete examples make the choice obvious:
The cart wants a list because the items are in a specific order and the operations are "add", "remove", "iterate". The prices want a dictionary because the question is "given a product name, what's the price?". The coordinate wants a tuple because the shape is fixed and immutable. The customer wants a dictionary because the fields have names and the record might change.
A common beginner instinct is to reach for parallel lists when a dictionary would be cleaner. Two lists, one of names and one of prices, where the i-th name matches the i-th price, can usually become one dictionary keyed on the name. That single change removes a whole class of bugs (the two lists getting out of sync) and makes lookups O(1) instead of O(n).
That said, parallel lists aren't always wrong. If the order matters and you'll iterate both lists together more often than you'll look up by name, a list (or a list of tuples) might be the right shape. The rule of thumb: when the dominant operation is lookup by label, a dictionary almost always wins.
One last thing worth knowing: starting in Python 3.7, dictionaries preserve insertion order. The pairs come back out in the same order you put them in. This wasn't always true; in older Python versions, dictionary iteration order was unpredictable.
The dictionary remembers that "Wireless Mouse" was added first, then "USB Cable", then "HDMI Cable", and prints them in that order. This is convenient for things like JSON output, where you want a stable field order. For now, just know that Python doesn't shuffle your keys behind your back.
10 quizzes