Python dictionaries are the workhorse of structured data: a mapping from keys to values that keeps insertion order, looks values up by name instead of by position, and refuses to hold a duplicate key. If you learn one container properly, make it this one.
What is a dictionary and when should you use one?
A dictionary stores key-value pairs. The key is a label you use to find the value, and the value is anything at all. The defining property is that the key does the addressing, so you never need to know the position of the thing you want.
user = {"name": "Ada", "role": "engineer", "active": True}
print(user["name"])
print(len(user))
print("role" in user)
Reach for a dictionary when the data has names: a user profile, a configuration object, a record from a database row, a JSON payload. Reach for a list when the data is a sequence: an ordered batch of similar things, a queue of jobs, a list of names.
The practical difference is in how you retrieve data. Looking an item up in a list means searching it until you find a match, and the code that does it has to express the search. Looking an item up in a dictionary is a direct operation, so the code reads as "give me the value for this key" with nothing else in the way.
How do you create a dictionary?
Curly braces with key: value pairs inside is the normal way. There is also a dict() constructor, which is the way to build one from pairs you already have.
literal = {"name": "Ada", "role": "engineer"}
pairs = [("name", "Ada"), ("role", "engineer")]
built = dict(pairs)
zipped = dict(zip(["name", "role"], ["Ada", "engineer"]))
print(literal)
print(built)
print(zipped)
zip pairs two sequences positionally, which is exactly how you turn two parallel lists into a mapping. Be careful with it: if the sequences have different lengths, zip stops at the shorter one and silently drops the extra values, so check the lengths when the data comes from somewhere you do not control.
An empty dictionary is {} and a non-empty one needs at least one pair. Note that {} is also how you write an empty set in other languages, but Python uses set() for that — the type of {} in Python is always a dictionary.
Why do dictionary keys have to be hashable?
Because a dictionary does not actually store its keys in order and look them up one by one. It computes a number from each key, hashes it, and uses that number to find a slot. That is why looking a key up does not depend on how many keys the dictionary holds, and why the keys must be values whose hash stays constant for the lifetime of the entry.
In practice that means keys must be immutable. Strings, numbers, booleans, None, tuples and frozen sets all work. Lists and dicts do not, because you could change them after using them as a key and the dictionary would lose track of where the entry lives.
try:
bad = {[1, 2]: "list key"}
except TypeError as exc:
print("TypeError:", exc)
good = {(1, 2): "tuple key", "name": "string key", 7: "int key"}
print(good)
Note that True and 1 are the same key as far as a dictionary is concerned, because True == 1 and they hash identically. That is rarely a problem in practice, but it is a good reason to be deliberate about key types.
How do you add, update and delete entries?
Assignment with square brackets creates a new entry or overwrites an existing one. update() merges in a batch of pairs, and deletion comes in three flavours.
settings = {"host": "localhost", "port": 5432}
settings["user"] = "admin"
settings.update({"port": 5433, "timeout": 30})
print(settings)
print("popped:", settings.pop("timeout"))
print("popped:", settings.pop("timeout", "not set"))
del settings["user"]
print(settings)
The three deletion tools differ in what they do when the key is missing. del settings["user"] raises KeyError. settings.pop("user") also raises KeyError, unless you give it a fallback as a second argument. The two-argument pop is the safe one for keys that may or may not be there.
One more thing to know: because dictionaries preserve insertion order, a key that already exists keeps its original position when you overwrite it. Adding a new key puts it at the end.
How do nested dictionaries work?
A value can be another dictionary, and you reach into it by chaining the bracket notation. This is the shape of most real configuration and most JSON documents.
profile = {
"user": {
"name": "Grace",
"email": "[email protected]",
"skills": ["python", "go"],
},
"active": True,
}
print(profile["user"]["name"])
print(profile["user"]["skills"][1])
print(profile["user"].get("email", "no email on file"))
Chained bracket lookups are powerful and slightly dangerous, because every level has to exist. If profile had no "user" key, the first lookup would raise KeyError before the second bracket was even reached. For data whose shape you do not control — an API response, a user-supplied file — reach for .get() at the levels that might be missing, which is the subject of the next lesson.
Nested structures also mutate in place. profile["user"]["skills"].append("rust") changes the inner list without rebinding anything, and there is no risk of losing the surrounding dictionary.
How do dictionaries compare to lists of pairs?
A list of two-element tuples holds the same information, and it is a perfectly reasonable structure to pass around. The difference shows up when you want to look something up. With a list you search; with a dictionary you ask.
pairs = [("name", "Ada"), ("role", "engineer")]
print(dict(pairs)["name"])
for key, value in pairs:
print(key, "=", value)
Reach for a list of pairs when the order matters and you always walk the whole thing. Reach for a dictionary when you mostly ask "what is the value for X?".
Dictionaries are also the natural shape for JSON. A JSON object becomes a Python dict and a Python dict becomes a JSON object, with the same keys and the same nesting, which makes them the default choice for anything that crosses a network boundary.
import json
payload = {"name": "Ada", "skills": ["python"], "active": True}
text = json.dumps(payload)
print(text)
print(json.loads(text) == payload)
Dict cheat sheet
| Operation | Code | Missing key |
|---|---|---|
| Read a value | d["k"] | raises KeyError |
| Read with a fallback | d.get("k", default) | returns the default |
| Add or overwrite | d["k"] = v | creates the entry |
| Merge several | d.update(other) | adds or overwrites |
| Delete | del d["k"] | raises KeyError |
| Delete with a fallback | d.pop("k", default) | returns the default |
| Check membership | "k" in d | returns False |
Try it yourself: build a dictionary describing a course with a title, an instructor, a list of topics and a nested dictionary of settings. Add a new topic, replace the instructor, and delete the settings key using pop with a fallback so nothing crashes if it is already gone. Then build a second dictionary from two parallel lists with zip and print it. Finally, try to use a list as a key and read the error message carefully — it tells you exactly which rule you broke.