Python Dictionaries Explained — Creation, Lookup, and Common Mistakes
Every app you've ever used is quietly powered by key-value pairs. When you log into a website, your username is looked up in a giant table to find your password hash. When Spotify loads your profile, it fetches your name, playlist count, and subscription tier all at once — not as a pile of unconnected numbers, but as named, organised data. Python dictionaries are the building block that makes this kind of organised, instant-access data possible in your own code.
Before dictionaries existed in Python (or before programmers reached for them), people stored related data in parallel lists — one list of names, one list of phone numbers, hoping the indexes stayed in sync. This is fragile, confusing, and breaks the moment someone inserts a row in the wrong place. A dictionary solves this by gluing the label and the value together permanently, so they can never drift apart.
By the end of this article you'll know how to create a dictionary from scratch, read and update values safely, loop through it without breaking anything, and avoid the three mistakes that trip up almost every beginner. You'll also know exactly when a dictionary is the right choice — and when it isn't.
Creating Your First Dictionary — and Understanding What You're Actually Building
A dictionary in Python is written with curly braces {}. Inside, you place pairs of items separated by a colon — the thing on the left of the colon is the key, the thing on the right is the value. Each pair is separated from the next by a comma.
Think of it this way: the key is the label on a filing cabinet drawer, and the value is the document inside. You always open the drawer by its label — never by guessing which drawer number it is.
Keys must be unique. You can't have two drawers with the same label — Python would just keep the last one you defined and silently drop the earlier one. Keys must also be immutable (unchangeable), which in practice means they're almost always strings or numbers. Values, on the other hand, can be absolutely anything: a number, a string, a list, even another dictionary.
You can also create an empty dictionary with just {} and fill it in later — useful when you're building data dynamically, like reading lines from a file.
# A dictionary representing a single student's record # Keys are strings (labels), values are mixed types student = { "name": "Maria Chen", # string value "age": 21, # integer value "gpa": 3.85, # float value "is_enrolled": True, # boolean value "courses": ["Math", "CS"] # list as a value — totally valid! } # Print the whole dictionary print(student) # Check what type it is print(type(student)) # Check how many key-value pairs are inside print("Number of fields:", len(student))
<class 'dict'>
Number of fields: 5
Reading, Adding, and Updating Values — The Three Core Operations You'll Use Every Day
Once your dictionary exists, you need to pull data out of it. The basic way is square bracket notation: student["name"] — think of it as typing the drawer label to pop it open.
But there's a trap with square brackets: if the key doesn't exist, Python throws a KeyError and your program crashes. The safer approach is the .get() method, which returns None by default (or a fallback value you choose) instead of crashing. In production code, .get() is almost always the right choice.
Adding a new key-value pair looks identical to updating an existing one — dictionary["new_key"] = value. If the key already exists, the value gets replaced. If it doesn't exist yet, a new pair is created. Python handles both cases with the same syntax, which keeps things simple.
Deleting a pair uses del dictionary["key"] or the .pop() method. The advantage of .pop() is that it returns the value you removed, so you can use it before it's gone — handy when you're moving data between structures.
student = {
"name": "Maria Chen",
"age": 21,
"gpa": 3.85
}
# --- READING ---
# Square bracket access — fast, but crashes if key is missing
print(student["name"]) # works fine
# .get() is the safe version — returns None if key doesn't exist
print(student.get("gpa")) # prints 3.85
print(student.get("major")) # prints None — no crash!
print(student.get("major", "Undeclared")) # custom fallback value
# --- ADDING a new key ---
student["major"] = "Computer Science" # key didn't exist — now it does
print(student)
# --- UPDATING an existing key ---
student["gpa"] = 3.92 # key already exists — value gets replaced
print("Updated GPA:", student["gpa"])
# --- DELETING a key ---
# Option 1: del — removes silently
del student["age"]
print("After del:", student)
# Option 2: .pop() — removes AND returns the value
gpa_at_graduation = student.pop("gpa")
print("Captured GPA before removing:", gpa_at_graduation)
print("Final record:", student)
3.85
None
Undeclared
{'name': 'Maria Chen', 'age': 21, 'gpa': 3.85, 'major': 'Computer Science'}
Updated GPA: 3.92
After del: {'name': 'Maria Chen', 'gpa': 3.92, 'major': 'Computer Science'}
Captured GPA before removing: 3.92
Final record: {'name': 'Maria Chen', 'major': 'Computer Science'}
Looping Through a Dictionary — Keys, Values, and Both at Once
Looping over a dictionary is something you'll do constantly — printing a report, transforming data, searching for a specific value. Python gives you three clean methods for this, and knowing which one to use when is a mark of a confident Python developer.
Looping with just for key in dictionary gives you the keys only — the drawer labels. From each key you can look up the value inside the loop if you need it.
The .values() method gives you just the values, no keys — useful when you want to sum up prices or check if a specific value exists anywhere.
The real power move is .items(), which gives you both the key and the value as a pair on every iteration. This is what you'll use 80% of the time because you usually need both. Python unpacks the pair into two variables automatically — for key, value in dictionary.items() — and it reads almost like plain English.
Dictionaries in Python 3.7 and later also maintain insertion order, so the loop will always visit pairs in the order you added them.
product_prices = {
"Laptop": 999.99,
"Mouse": 29.99,
"Keyboard": 79.99,
"Monitor": 349.99
}
# --- Loop 1: Keys only ---
print("=== Product Names ===")
for product_name in product_prices:
print("-", product_name) # each iteration gives us one key
# --- Loop 2: Values only ---
print("\n=== All Prices ===")
total = 0
for price in product_prices.values():
total += price # accumulate a running total
print(f"Total inventory value: ${total:.2f}")
# --- Loop 3: Keys AND Values together (the most common pattern) ---
print("\n=== Full Price List ===")
for product_name, price in product_prices.items(): # unpacks each pair
if price > 100:
label = "Premium" # flag expensive items
else:
label = "Budget"
print(f"{product_name}: ${price:.2f} ({label})")
# --- Checking if a key exists before accessing it ---
search_term = "Webcam"
if search_term in product_prices: # 'in' checks keys by default
print(f"\nFound {search_term}: ${product_prices[search_term]}")
else:
print(f"\n'{search_term}' is not in our catalogue.")
- Laptop
- Mouse
- Keyboard
- Monitor
=== All Prices ===
Total inventory value: $1459.96
=== Full Price List ===
Laptop: $999.99 (Premium)
Mouse: $29.99 (Budget)
Keyboard: $79.99 (Budget)
Monitor: $349.99 (Premium)
'Webcam' is not in our catalogue.
Nested Dictionaries — Storing Complex, Real-World Data Structures
Real data is rarely flat. A user doesn't just have a name — they have an address, which itself has a street, city, and postcode. A dictionary can hold another dictionary as a value, letting you model this hierarchy naturally.
This is called a nested dictionary. You access values inside it by chaining square brackets or .get() calls: user["address"]["city"]. Each set of brackets digs one level deeper — like opening a folder inside a folder.
Nested dictionaries are everywhere in professional Python: JSON data from an API is almost always a nested dictionary (Python's json module converts it automatically). Configuration files, database records, and game state are all commonly stored this way.
One thing to watch when working with nested data: if an intermediate key doesn't exist, chaining square brackets will crash on the first missing key before it even tries the inner one. Using .get() at each level prevents this — or you can use a try/except block if the structure is deeply uncertain.
# A dictionary of users — each key is a user ID, each value is another dictionary user_database = { "usr_001": { "username": "mariachen", "email": "maria@example.com", "address": { "street": "14 Elm Street", "city": "Austin", "postcode": "73301" }, "is_premium": True }, "usr_002": { "username": "jakethompson", "email": "jake@example.com", "address": { "street": "9 Oak Avenue", "city": "Denver", "postcode": "80201" }, "is_premium": False } } # Access a top-level value print(user_database["usr_001"]["username"]) # chain brackets to go deeper # Access a deeply nested value print(user_database["usr_001"]["address"]["city"]) # Safe access with .get() at each level — no crash if a key is missing user = user_database.get("usr_003", {}) # returns empty dict if not found city = user.get("address", {}).get("city", "Unknown") print("City for usr_003:", city) # gracefully returns 'Unknown' # Loop through all users and print a summary print("\n=== User Summary ===") for user_id, user_info in user_database.items(): tier = "Premium" if user_info["is_premium"] else "Free" city = user_info["address"]["city"] print(f"{user_id} | @{user_info['username']} | {city} | {tier}")
Austin
City for usr_003: Unknown
=== User Summary ===
usr_001 | @mariachen | Austin | Premium
usr_002 | @jakethompson | Denver | Free
| Feature / Aspect | Python Dictionary | Python List |
|---|---|---|
| How you access data | By a named key: dict['name'] | By a position number: list[0] |
| Order guaranteed? | Yes — insertion order kept (Python 3.7+) | Yes — always ordered by index |
| Lookup speed | O(1) — instant, regardless of size | O(n) — slower as the list grows (for searching) |
| Duplicate keys allowed? | No — last assignment wins silently | Yes — duplicate values are fine |
| Best used when | You need to label your data (name, price, id) | You have a sequence of similar items (scores, names) |
| Key types allowed | Immutable only: strings, numbers, tuples | Not applicable — lists use integer indexes |
| Memory usage | Slightly higher due to hash table overhead | Lower — compact sequential storage |
🎯 Key Takeaways
- A dictionary stores data as key-value pairs — use it any time your data has labels (name, price, id) rather than just a sequence of items.
- Always use .get() instead of square brackets when the key might not exist — it prevents KeyError crashes and lets you define a sensible fallback value.
- Dictionary lookups are O(1) — Python uses a hash table internally, meaning it jumps directly to the value regardless of how many pairs are stored.
- Nested dictionaries (a dictionary inside a dictionary) are the natural way to model real-world hierarchical data like users, addresses, or JSON API responses.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using a mutable type (like a list) as a dictionary key — You'll get a
TypeError: unhashable type: 'list'immediately. Python requires keys to be immutable because it calculates a hash from the key to find the value instantly. Fix it by converting the list to a tuple first: use(1, 2, 3)as your key instead of[1, 2, 3]. - ✕Mistake 2: Accessing a key that might not exist with square brackets instead of .get() — This raises a
KeyErrorand crashes your program, which is especially painful when processing user input or API data where keys aren't guaranteed. Fix it by switching to.get('key', fallback_value)— it returnsNone(or your chosen default) instead of crashing. - ✕Mistake 3: Modifying a dictionary while iterating over it — Python raises
RuntimeError: dictionary changed size during iterationif you add or delete keys inside afor key in dictionaryloop. Fix it by iterating over a snapshot of the keys:for key in list(dictionary.keys())— this creates a static copy of the keys first, so adding or removing from the original dictionary during the loop is safe.
Interview Questions on This Topic
- QWhat's the difference between dict['key'] and dict.get('key'), and when would you choose one over the other in production code?
- QDictionaries are described as O(1) for lookups — can you explain why that's the case, and what data structure Python uses under the hood to achieve it?
- QIf you have two large lists — one of employee names and one of their salaries — and you need to frequently look up a salary by name, how would you restructure this data and why?
Frequently Asked Questions
Can a Python dictionary have duplicate keys?
No — dictionary keys must be unique. If you define the same key twice, Python doesn't raise an error; it silently keeps the last value assigned to that key and discards the earlier one. This is a common source of subtle bugs, so always make sure your keys are distinct.
What types can be used as dictionary keys in Python?
Only immutable (unchangeable) types can be keys — strings, integers, floats, and tuples are all valid. Lists and dictionaries cannot be keys because they can be changed after creation, which would break Python's internal hash table. If you need a sequence as a key, convert it to a tuple first.
Is a Python dictionary ordered? Will my keys always come back in the same order?
Yes, from Python 3.7 onwards dictionaries are guaranteed to maintain insertion order — the order you added the key-value pairs is the order you'll get them back when you loop or print. In Python 3.6 and earlier this was not guaranteed, so if you're on a modern Python version (which you almost certainly are), you can rely on insertion order.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.