python dictionaries are arguably the single most useful data structure in the language — a collection built specifically for looking things up by a meaningful name instead of a numeric position. This article covers how they work, the essential python dictionary methods, and the comprehension syntax that lets you build them in a single line.
What is a Dictionary?
A dictionary is a collection that maps unique keys to values. Unlike a list, where you access items by numeric position, a dictionary lets you access items by a meaningful key you choose yourself.
c {"name": "Alex", "phone": "555-0142"}Real-world framing
Think of it like a contact list on your phone: you don't scroll through numbered slots to find someone — you look up their name and get their phone number back. That's exactly the relationship a dictionary models: a key (the name) mapping to a value (the number).
Ordering
Dictionaries are ordered by insertion — as of Python 3.7, this behavior is officially guaranteed, not just an implementation detail. Items appear in the order you added them when you iterate over the dictionary. That said, dictionaries aren't ordered in the sense that lists are — you still access values by key, not by numeric position, so "ordered" here refers specifically to iteration order, not indexed access.
Creating dictionaries
# Literal syntax
person = {"name": "Alex", "age": 30}
# The dict() constructor
person = dict(name="Alex", age=30)
# Dictionary comprehension — covered in more depth in Section 4
squares = {n: n ** 2 for n in range(5)}Why keys must be immutable, but values can be anything
As covered in the earlier tuples and sets articles, dictionary keys need to be hashable — which generally means immutable: strings, numbers, and tuples all work fine as keys. Values, on the other hand, have no such restriction — a value can be a string, a number, a list, another dictionary, anything at all:
person = {
"name": "Alex",
"hobbies": ["reading", "hiking"], # a list as a value — perfectly fine
"address": {"city": "Nagpur", "zip": "440001"} # a nested dict as a value — also fine
}Accessing and Modifying Data
Retrieving values: [] vs. .get()
person = {"name": "Alex", "age": 30}
print(person["name"]) # AlexSquare-bracket access works fine — right up until you request a key that doesn't exist:
print(person["email"])
# KeyError: 'email'A KeyError is python's way of saying "this key doesn't exist," and it will crash your program if left unhandled. The safer alternative is .get(), which returns None (or a default value you specify) instead of raising an error:
print(person.get("email")) # None — no error
print(person.get("email", "N/A")) # N/A — a custom fallback value.get() is generally the preferred way to access a dictionary value whenever there's any chance the key might not be present — which, in real-world code working with external data, is genuinely common.
Adding and updating entries
person = {"name": "Alex"}
# Add or update a single key directly
person["age"] = 30
print(person) # {'name': 'Alex', 'age': 30}
person["age"] = 31 # updating an existing key works the exact same way
print(person) # {'name': 'Alex', 'age': 31}
# Add or update multiple keys at once
person.update({"city": "Nagpur", "age": 32})
print(person) # {'name': 'Alex', 'age': 32, 'city': 'Nagpur'}Notice that assigning to an existing key and adding a new key use identical syntax — Python figures out which one you mean based on whether the key already exists.
Deleting entries
person = {"name": "Alex", "age": 30, "city": "Nagpur"}
# del — removes a key, doesn't return anything
del person["city"]
print(person) # {'name': 'Alex', 'age': 30}
# pop() — removes a key AND returns its value
age = person.pop("age")
print(age, person) # 30 {'name': 'Alex'}.pop() also accepts a default value, mirroring .get()'s safety, letting you avoid a KeyError if the key might not exist: person.pop("email", "not found").
setdefault(): insert only if missing
.setdefault() is a genuinely useful, slightly less obvious method: it returns a key's value if it exists, but if it doesn't, it inserts the key with a default value and returns that default — all in one call.
person = {"name": "Alex"}
age = person.setdefault("age", 0)
print(age) # 0 — key didn't exist, so it was added with the default
print(person) # {'name': 'Alex', 'age': 0}
age = person.setdefault("age", 99)
print(age) # 0 — key already existed, so the default was ignoredThis is particularly useful for building nested structures incrementally — for example, grouping items into lists keyed by category, where you're not sure yet whether a given category's list already exists:
groups = {}
items = [("fruit", "apple"), ("vegetable", "carrot"), ("fruit", "banana")]
for category, item in items:
groups.setdefault(category, []).append(item)
print(groups) # {'fruit': ['apple', 'banana'], 'vegetable': ['carrot']}Without .setdefault(), this pattern would require an extra if category not in groups: check before every append.
Core Methods for Traversing a Dictionary
keys(), values(), and items()
person = {"name": "Alex", "age": 30}
print(person.keys()) # dict_keys(['name', 'age'])
print(person.values()) # dict_values(['Alex', 30])
print(person.items()) # dict_items([('name', 'Alex'), ('age', 30)]).keys()— every key in the dictionary..values()— every value, without their corresponding keys..items()— key-value pairs together, as tuples.
Looping patterns
person = {"name": "Alex", "age": 30}
# Iterating a dictionary directly gives you the keys (same as calling .keys())
for key in person:
print(key)
# name
# age
# Iterating values directly
for value in person.values():
print(value)
# Alex
# 30
# Unpacking key-value pairs together — the most commonly used pattern
for key, value in person.items():
print(f"{key}: {value}")
# name: Alex
# age: 30That last pattern — for key, value in person.items(): — is the one you'll write constantly once you're working with dictionaries in real code.
Practical example: word-frequency counting
A genuinely common real-world task, combining .get() with a loop:
text = "the quick brown fox jumps over the lazy dog the fox runs"
words = text.split()
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
# {'the': 3, 'quick': 1, 'brown': 1, 'fox': 2, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1, 'runs': 1}counts.get(word, 0) returns the word's current count if it's already been seen, or 0 if it's brand new — either way, adding 1 and reassigning handles both cases in a single line, without a separate if word in counts: check.
Dictionary Comprehensions and Merging
Building dictionaries in one line
Dictionary comprehensions mirror the list comprehension syntax covered in an earlier article, but produce key-value pairs instead of single values:
squares = {n: n ** 2 for n in range(5)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}You can add a filtering condition, just like with list comprehensions:
even_squares = {n: n ** 2 for n in range(10) if n % 2 == 0}
print(even_squares) # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}Classic patterns
Inverting a dictionary — swapping keys and values:
original = {"a": 1, "b": 2, "c": 3}
inverted = {value: key for key, value in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}(This only works cleanly if the original values are themselves unique and hashable — duplicate values would silently overwrite each other in the inverted result.)
Grouping data by a shared attribute:
people = [
{"name": "Alex", "dept": "Engineering"},
{"name": "Sam", "dept": "Sales"},
{"name": "Jordan", "dept": "Engineering"},
]
by_dept = {}
for person in people:
by_dept.setdefault(person["dept"], []).append(person["name"])
print(by_dept) # {'Engineering': ['Alex', 'Jordan'], 'Sales': ['Sam']}Merging dictionaries
Python has accumulated a few different ways to merge dictionaries over the years, and it's worth knowing all three:
a = {"x": 1, "y": 2}
b = {"y": 3, "z": 4}
# Modern approach — the | merge operator (Python 3.9+)
merged = a | b
print(merged) # {'x': 1, 'y': 3, 'z': 4}
# Older approach — dictionary unpacking
merged = {**a, **b}
print(merged) # {'x': 1, 'y': 3, 'z': 4}
# Method approach — mutates 'a' directly, rather than creating a new dict
a.update(b)
print(a) # {'x': 1, 'y': 3, 'z': 4}In every case, when both dictionaries share a key (like "y" here), the value from the second dictionary (or the argument passed to .update()) wins. The | operator is the current recommended approach for creating a new merged dictionary if you're on Python 3.9 or later; {**a, **b} remains a solid, widely compatible alternative for older versions; .update() is specifically for when you want to modify one of the dictionaries in place rather than creating a new one.
Why Dictionaries Are Fast (and Practical Use Cases)
Under the hood: hash tables
Like sets, dictionaries are backed by a hash table. This gives them average O(1) lookup time — checking or retrieving a value by key takes roughly the same amount of time no matter how large the dictionary grows. Compare that to searching for a matching item in a list, which is O(n) — the larger the list, the longer a search potentially takes, since Python may need to check every single item. This is precisely why dictionaries (and sets) are the right structure any time you need fast lookups by some identifying key, rather than a plain list.
Real-world use cases
Parsing JSON and API responses — nearly every API response you'll work with in Python arrives as nested dictionaries and lists; understanding dictionary access and nested structures directly translates to working comfortably with real-world data.
Configuration management — application settings are a natural fit for key-value storage.
Caching and memoization — storing previously computed results keyed by their input, so expensive calculations don't need to be repeated.
Grouping and counting data — as shown in the word-frequency and department-grouping examples above.
A note on nested dictionaries
Because values can be anything — including other dictionaries — you'll frequently encounter nested structures, especially when working with real-world data like API responses:
user = {
"name": "Alex",
"address": {
"city": "Nagpur",
"zip": "440001"
}
}
print(user["address"]["city"]) # NagpurAccessing nested values just means chaining the bracket access — user["address"]["city"] reaches into the outer dictionary, then into the nested one.
Common pitfall: mutable keys
Just like sets, dictionary keys must be hashable, which effectively means immutable. Trying to use a list as a key raises an error:
data = {}
data[[1, 2]] = "invalid"
# TypeError: unhashable type: 'list'If you need a compound key made of multiple values, use a tuple instead — tuples are hashable (assuming their own contents are), and this is one of their most common practical uses, as covered in the earlier tuples article:
data = {}
data[(1, 2)] = "valid" # a tuple works fine as a key
print(data) # {(1, 2): 'valid'}