Dictionaries in Python
A Python dictionary stores key-value pairs and looks up values by key almost instantly using a hashtable. Keys must be immutable (strings, numbers, tuples) — lists can never be keys, though they can be values.
A list has one frustrating limitation: the only way to find something inside it is by position. What if you want to look up a word and get back its meaning, or a name and get back a phone number? Searching a list item by item for that would be slow — and this is exactly why dictionaries exist.
A Python dictionary stores key-value pairs, and looking something up by its key is nearly instant, no matter how large the dictionary grows. This page covers creating and using dictionaries, the histogram counting pattern, why lists can never be keys, and how dictionaries power a genuinely powerful optimization trick called memoization. Pair it with Lists in Python for the sequence type dictionaries are usually compared against.
What you will learn
By the end you can:
- Create dictionaries and look up, add, and delete key-value pairs
- Use the histogram pattern to count things with a dictionary
- Loop through a dictionary's keys, values, and items
- Explain why lists can never be dictionary keys
- Understand memoization and how to work with global variables safely
Creating and using dictionaries
A dictionary is a mapping between keys and values. Each key maps to exactly one value; a key and its value together are called a key-value pair, or sometimes an item. Create an empty dictionary with dict() or empty curly braces:
eng2sp = dict()
print(eng2sp)
# {}To add a key-value pair, use square brackets on the left of an assignment — almost like indexing into an empty slot:
eng2sp['one'] = 'uno'
print(eng2sp)
# {'one': 'uno'}You can also build a dictionary with several items already inside, all at once:
eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}Look up a value using its key in square brackets. If the key does not exist, Python immediately raises a KeyError — it never silently returns None:
print(eng2sp['two'])
# 'dos'
print(eng2sp['four'])
# KeyError: 'four'A few other handy operations:
len(eng2sp) # 3 — number of key-value pairs
'one' in eng2sp # True — checks keys, not values
'uno' in eng2sp # False — 'uno' is a value, not a key
vals = eng2sp.values()
'uno' in vals # True — check membership in values insteadin is dramatically faster on dictionaries than on lists. A list check has to scan every element one at a time; a dictionary uses a hashtable that finds the answer in roughly the same tiny amount of time whether the dictionary has 10 items or 10 million.
Counting with a dictionary (the histogram pattern)
One of the most useful things you can do with a dictionary is count how often something appears. The clean, idiomatic way to count letters in a string: use a dictionary where keys are letters and values are their counts — you never have to know in advance which letters will show up.
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] += 1
return d
h = histogram('brontosaurus')
print(h)
# {'b': 1, 'r': 2, 'o': 2, 'n': 1, 't': 1, 's': 2, 'a': 1, 'u': 2}"Histogram" is a statistics term meaning a set of counters — exactly what this is, a frequency map of every character in the string.
A cleaner version with .get()
.get() returns a default value when a key does not exist, instead of raising a KeyError — this eliminates the if/else:
h.get('a', 0) # 1 — 'a' exists
h.get('b', 0) # 0 — 'b' doesn't exist, returns the fallback
def histogram(s):
d = dict()
for c in s:
d[c] = d.get(c, 0) + 1
return dd.get(c, 0) + 1 reads as "give me the current count for c, or 0 if it isn't there yet, then add 1." This pattern shows up constantly in real Python code.
Looping through a dictionary
A for loop over a dictionary loops through its keys:
def print_hist(h):
for c in h:
print(c, h[c])For a guaranteed order, sort the keys yourself. .keys() returns a view object in Python 3, not a plain list, so wrap it in sorted():
def print_hist_sorted(h):
keys = sorted(h.keys())
for k in keys:
print(k, h[k])Reverse lookup
A normal lookup gives you the value for a key. A reverse lookup is the opposite — you have a value, and want the key that maps to it. There is no built-in shortcut; you search manually:
def reverse_lookup(d, v):
for k in d:
if d[k] == v:
return k
raise ValueError('value does not appear in the dictionary')
h = histogram('parrot')
reverse_lookup(h, 2) # 'r' — 'r' appears twice
reverse_lookup(h, 3) # ValueError: value does not appear in the dictionaryYou can raise exceptions yourself, on purpose, using raise — this is how well-written functions communicate failure clearly instead of silently returning a wrong answer. Reverse lookups are genuinely slower than forward lookups, since there is no hashtable shortcut going in that direction — check every value, one at a time.
Why lists can't be dictionary keys
Dictionary values can be anything — including lists, which is useful for grouping multiple values under one key. But lists can never be keys:
t = [1, 2, 3]
d = dict()
d[t] = 'oops'
# TypeError: unhashable type: 'list'Dictionaries work internally using a hashtable, which computes a numeric fingerprint (a hash) for each key to decide where to store it. That hash must never change after storage. Immutable types — strings, integers, tuples — always have a stable hash. Mutable types like lists could change after being stored, breaking the whole system. The rule: only hashable (immutable) types can be dictionary keys. Tuples work fine as keys; lists and other dictionaries do not.
Memoization: teaching a function to remember
A plain recursive Fibonacci function recalculates the same values over and over — fibonacci(2) might get computed dozens of times inside a single call, and it gets exponentially worse for larger inputs. Memoization fixes this by storing results you have already worked out, using a dictionary:
known = {0: 0, 1: 1}
def fibonacci(n):
if n in known:
return known[n]
res = fibonacci(n-1) + fibonacci(n-2)
known[n] = res
return res
fibonacci(10) # 55
fibonacci(50) # 12586269025 — still fastknown stores every Fibonacci number already computed. Each call first checks known; if the answer is there, it returns immediately with no further work. Otherwise it computes it, saves it, and returns it. Now each value only ever gets computed once — a dictionary is the perfect tool for this.
Global variables
In the memoization example, known is defined outside any function — a global variable, which persists for the entire program's lifetime rather than disappearing when a function returns. Any function can freely read a global with no special syntax:
verbose = True
def example1():
if verbose:
print('Running example1')Reassigning a global from inside a function is where the trap lives:
been_called = False
def example2():
been_called = True # WRONG — creates a new local variable instead
example2()
print(been_called)
# False — nothing actually changedPython assumes any assignment inside a function creates a new local variable — it never touches the global one. To genuinely modify a global, use the global keyword:
been_called = False
def example2():
global been_called
been_called = True # now it actually works
example2()
print(been_called)
# TrueUseful dictionary methods
.keys(), .values(), and .items() give you views over a dictionary's contents. .items() is especially useful for looping over both the key and value together:
d = {'a': 1, 'b': 2, 'c': 3}
for key, value in d.items():
print(key, 'maps to', value).pop() removes a key and returns its value:
d = {'a': 1, 'b': 2}
val = d.pop('a')
print(val) # 1
print(d) # {'b': 2}.update() merges another dictionary in, overwriting shared keys:
d1 = {'a': 1, 'b': 2}
d2 = {'b': 99, 'c': 3}
d1.update(d2)
print(d1) # {'a': 1, 'b': 99, 'c': 3}.setdefault() sets a key only if it is not already there — the same pattern used inside histogram earlier:
d = {'a': 1}
d.setdefault('a', 99) # 'a' exists — no change, returns 1
d.setdefault('b', 99) # 'b' is new — sets it to 99, returns 99
print(d) # {'a': 1, 'b': 99}Dictionary comprehensions (a preview)
Like list comprehensions, Python has a compact way to build a dictionary in one line:
words = ['apple', 'banana', 'cherry']
lengths = {word: len(word) for word in words}
print(lengths)
# {'apple': 5, 'banana': 6, 'cherry': 6}The general shape is {key_expr: value_expr for item in iterable} — the same logic as a loop that builds up a dictionary, written as a single expression.
Common mistakes
- Assuming
'value' in dchecks values —inchecks keys by default - Using a list as a dictionary key and hitting
TypeError: unhashable type - Reassigning a global inside a function without
global, silently creating a local instead - Reading and writing the same global in one line (
count = count + 1) without declaringglobal countfirst - Expecting
.keys()in Python 3 to behave like a plain list — it's a view, wrap it insorted()orlist()
Why this matters
Dictionaries are the workhorse of real Python programs — counting, caching, configuration, JSON data, and anything shaped like "look this up by name." Understanding hashable keys and mutability here prevents bugs that only show up once your data gets large or your recursion gets deep.
Common questions
What is a dictionary in Python?
A dictionary is a mapping of key-value pairs. You look up a value using its key, e.g. d['one'], and lookups are nearly instant regardless of how large the dictionary is, thanks to an internal hashtable.
Why can't you use a list as a dictionary key in Python?
Dictionary keys must be hashable, meaning their hash value never changes. Lists are mutable and can change after creation, which would break the hashtable, so Python raises TypeError: unhashable type: 'list'.
What is the difference between dict.get() and d[key] in Python?
d[key] raises a KeyError if the key doesn't exist. d.get(key, default) returns a fallback value instead of crashing, making it safer when a key might be missing.
What is memoization in Python?
Memoization is caching the results of expensive function calls (often recursive ones like Fibonacci) in a dictionary, so repeated calls with the same input return instantly instead of recalculating.
How do you modify a global variable inside a Python function?
Declare it with the global keyword inside the function first, e.g. global count, then assign to it. Without global, an assignment creates a new local variable instead of changing the global one.
How do you loop through a dictionary's keys and values together?
Use .items(), e.g. for key, value in d.items(): print(key, value). This gives you both the key and its value on each pass, instead of only the key.
Explore
Related topics
Keep going — these sit next to this concept in a real learning path.
Lists in Python
The mutable sequence type dictionaries are usually compared against.
Strings in Python
Why strings are hashable and can be used as dictionary keys.
Data Types in Python
Where dict fits among Python's mutable, non-primitive types.
Types, Values, and Errors in Python
More on KeyError, TypeError, and how Python errors work.
Tuples in Python
Immutability, unpacking, *args, zip(), and DSU sorting.
Sets in Python
Uniqueness, fast membership checks, and set math.
Python course hub
All free Python explainers and the path into Agentic practice.
More Tools for Iteration in Python
continue, enumerate(), zip(), comprehensions, and generators.
Classes and Objects in Python
Defining classes, attributes, embedded objects, and copying.
File Handling in Python (Reading, Writing, CSV & JSON)
open(), with, reading modes, CSV, and JSON.