Dictionaries
Data with names instead of positions: create, read safely, update, remove, and loop over key-value pairs.
What you will be able to do
- Explain what a dictionary is and why named keys beat list positions for records
- Create dictionaries with {}, dict(), and key: value pairs
- Read values with [] and with get(), and know which one raises KeyError
- Add, update, and merge data with assignment and update()
- Remove data with pop(), del, popitem(), and clear()
- Check whether a key exists with in
- Loop over keys, values, and key-value pairs with items()
- Work with nested dictionaries and lists of dictionaries - the shape of JSON
- Choose between a list, a set, and a dictionary from what the data has to do
The idea, in plain English
A dictionary stores data as key-value pairs. student = {"name": "Chandu", "age": 34, "city": "Hyderabad"} has three keys - "name", "age", "city" - and each key leads to one value. The key is on the left of the colon, the value on the right.
Compare the same record as a list: ["Chandu", 34, "Hyderabad"]. To read the city you write student[2] and have to remember that position 2 means city. With a dictionary you write student["city"], and the code says what it means. That is the whole reason dictionaries exist for records: names instead of positions.
Under the hood a dictionary is a hash table, like a set. Python hashes the key and jumps straight to its value, so lookup by key is fast however large the dictionary grows. The price is the same as for sets: keys must be unique and hashable - strings, numbers, tuples - never lists.
Since Python 3.7 a dictionary also remembers insertion order, so printing and looping show keys in the order you added them. Dictionaries are everywhere in real Python: JSON from an API, rows from a database, configuration, and any record whose fields have names.
Worked example: Modelling a user record, then reading it safely.
Creating a dictionary
Curly braces with key: value pairs, separated by commas. {} on its own is an empty dictionary - which is exactly why the empty set had to be written set() in Lesson 18. dict() also makes an empty one, and can build from keyword arguments or a list of pairs.
Keys are usually strings, but any hashable value works: numbers, booleans, tuples. A list cannot be a key - {["a"]: 1} raises TypeError: unhashable type: ‘list’. Values can be anything at all: numbers, strings, booleans, lists, other dictionaries.
{}An empty dictionary. Not a set.dict()Also an empty dictionary.{"name": "Ravi", "age": 30}A literal with two key-value pairs.dict(name="Ravi", age=30)Keyword arguments become string keys: {‘name’: ‘Ravi’, ‘age’: 30}.dict([("name", "Ravi"), ("age", 30)])From a list of (key, value) pairs.dict.fromkeys(["a", "b"], 0)Every key with the same starting value: {‘a’: 0, ‘b’: 0}.Reading values: [] or get()
user["name"] returns the value for "name". If the key is not there, it raises KeyError, and the message names the missing key: KeyError: ‘city’. Use [] when the key must be there - a missing key is then a bug you want to hear about.
user.get("city") returns None instead of raising, and user.get("city", "Unknown") returns your default. Use get() for data that is genuinely optional - an email a user may not have given. get() only reads: it does not add the key, so the dictionary is unchanged afterwards.
user["name"]‘Ravi’user["city"]KeyError: ‘city’user.get("city")None - no error.user.get("city", "Unknown")‘Unknown’ - and user still has only two keys.user[0]KeyError: 0 - there are no positions; 0 is looked up as a key.user["Name"]KeyError: ‘Name’ - keys are case-sensitive.Adding, updating and merging
Assignment does both jobs: user["city"] = "Hyderabad" adds the key if it is new, and replaces the value if it already exists. There is no separate "insert" - which is convenient, and also how a typo in a key name silently creates a second key instead of updating the one you meant.
update() applies several at once, from another dictionary or keyword arguments: user.update({"age": 30, "city": "Hyderabad"}) or user.update(age=31). Keys that exist are overwritten, new ones are added. Python 3.9+ also has | to build a merged new dictionary: defaults | overrides, where the right side wins.
Removing data
pop(key) removes the key and returns its value - user.pop("age") returns 30. del user["city"] removes without returning anything. Both raise KeyError when the key is missing; pop(key, default) is the safe form and returns the default instead.
popitem() removes and returns the last-inserted pair as a tuple, and clear() empties the dictionary but keeps the same object.
pop("age")Remove and return the value. KeyError if missing.pop("phone", None)Safe: returns None when the key is not there.del user["city"]Remove, return nothing. KeyError if missing.popitem()Remove and return the last pair added, e.g. (‘c’, 3).clear()Remove everything. print shows {}.Checking keys, and looping
"name" in user checks the keys - not the values. "Ravi" in user is False even when "Ravi" is a value; use "Ravi" in user.values() for that. Like a set, the key check is fast however big the dictionary is.
Looping over a dictionary gives its keys, in insertion order. user.values() gives the values and user.items() gives (key, value) pairs, which you unpack: for key, value in user.items(). That last form is the one you will write most.
keys(), values() and items() return views - dict_keys([...]) and friends - not lists. A view follows the dictionary as it changes. Wrap it in list() when you need an actual list, and never add or remove keys while looping over the same dictionary: Python stops with RuntimeError: dictionary changed size during iteration.
user.keys()dict_keys(['name', 'age', 'city'])user.values()dict_values(['Ravi', 30, 'Hyderabad'])user.items()dict_items([('name', 'Ravi'), ('age', 30), ('city', 'Hyderabad')])Keys are unique, values need not be
Write the same key twice and the last value wins: {"name": "Ravi", "name": "Chandu"} is {‘name’: ‘Chandu’}. Python does not warn you. The key keeps the position of its first appearance: {"name": "Ravi", "age": 30, "name": "Chandu"} prints {‘name’: ‘Chandu’, ‘age’: 30}.
Values can repeat freely - three students taking "Python" is three keys with the same value. One surprise worth knowing: 1 and True are equal and hash the same, so {1: "one", True: "true"} has a single key.
Nested dictionaries and lists of dictionaries
A value can itself be a dictionary: user["address"]["city"]. Read the brackets left to right - first the address dictionary, then the city inside it. This is the shape of JSON, so it is the shape of most API responses.
A list of dictionaries is the other everyday shape: users[0]["name"] is the first user’s name, and for user in users: print(user["name"]) walks them all. With nesting, a missing level raises KeyError for that level - user["profile"]["bio"] fails on ‘profile’. Chain get() with an empty dictionary as the default when a level may be missing: user.get("profile", {}).get("bio", "n/a").
copy() is shallow
b = a does not copy anything - both names point at the same dictionary, so b["y"] = 2 changes a too. a.copy() makes a new dictionary, but its values are the same objects: if a value is a list, both dictionaries share that list, and appending to it through the copy changes the original.
When you need fully independent nested data, use copy.deepcopy(a). For a flat dictionary of strings and numbers, copy() is all you need.
Watch out: emp.copy() then copy["skills"].append("AWS") also adds AWS to emp["skills"]. The copy has its own keys, not its own lists.
Dictionary, list or set?
A list is accessed by position, a dictionary by key, and a set has no access at all - only membership. {} is a dictionary; an empty set is set().
ListOrdered values reached by position: users[0]. For sequences.DictionaryValues reached by a meaningful key: user["name"]. For records and lookups.SetUnique values, membership and comparison only: "admin" in roles.Fast membershipDictionary (on keys) and set - O(1) on average. List - O(n).EmptyList [] · dictionary {} or dict() · set set().Syntax and examples
student = {
"name": "Chandu",
"age": 34,
"city": "Hyderabad",
}
print(student["name"]) # Chandu
print(student["city"]) # Hyderabad
print(student) # {'name': 'Chandu', 'age': 34, 'city': 'Hyderabad'}
# The same record as a list - which position was the city?
as_list = ["Chandu", 34, "Hyderabad"]
print(as_list[2]) # Hyderabad
print(type({})) # <class 'dict'>
print(type(dict())) # <class 'dict'>
print(dict(name="Ravi", age=30)) # {'name': 'Ravi', 'age': 30}user = {"name": "Ravi", "age": 30}
print(user.get("name")) # Ravi
print(user.get("city")) # None
print(user.get("city", "Unknown")) # Unknown
print(user) # {'name': 'Ravi', 'age': 30} - get() added nothing
# print(user["city"]) -> KeyError: 'city'
if "email" in user:
print(user["email"])
else:
print("Email not available") # Email not availableuser = {"name": "Ravi", "age": 30}
user["city"] = "Hyderabad" # new key - added
user["age"] = 31 # existing key - updated
print(user) # {'name': 'Ravi', 'age': 31, 'city': 'Hyderabad'}
user.update({"age": 30, "email": "ravi@example.com"})
print(user) # {'name': 'Ravi', 'age': 30, 'city': 'Hyderabad', 'email': 'ravi@example.com'}
age = user.pop("age")
print(age) # 30
print(user.pop("phone", None)) # None - the safe form
del user["email"]
print(user) # {'name': 'Ravi', 'city': 'Hyderabad'}
user.clear()
print(user) # {}user = {"name": "Ravi", "age": 30, "city": "Hyderabad"}
print(user.keys()) # dict_keys(['name', 'age', 'city'])
print(list(user.keys())) # ['name', 'age', 'city']
print(list(user.values())) # ['Ravi', 30, 'Hyderabad']
print(list(user.items())) # [('name', 'Ravi'), ('age', 30), ('city', 'Hyderabad')]
print("name" in user) # True - checks keys
print("Ravi" in user) # False - values are not keys
print("Ravi" in user.values()) # True
print(len(user)) # 3
for key, value in user.items():
print(key, value)
# name Ravi
# age 30
# city Hyderabadprint({"name": "Ravi", "name": "Chandu"})
# {'name': 'Chandu'} - the last value wins, without a warning
print({"name": "Ravi", "age": 30, "name": "Chandu"})
# {'name': 'Chandu', 'age': 30} - and the key keeps its first position
print({1: "one", True: "true"})
# {1: 'true'} - 1 and True are the same key
point = {(1, 2): "start"}
print(point[(1, 2)]) # start - tuples can be keys
# {["a"]: 1} -> TypeError: unhashable type: 'list'user = {
"name": "Ravi",
"skills": ["Python", "React", "AWS"],
"address": {"city": "Hyderabad", "country": "India"},
}
print(user["skills"][1]) # React
print(user["address"]["city"]) # Hyderabad
print(user.get("profile", {}).get("bio", "n/a")) # n/a - a missing level, safely
users = [
{"id": 1, "name": "Ravi"},
{"id": 2, "name": "Kiran"},
]
print(users[0]["name"]) # Ravi
for item in users:
print(item["name"])
# Ravi
# Kiranuser = {"id": 101, "name": "Chandu", "age": 34, "city": "Hyderabad"}
print(user["name"]) # Chandu
user["email"] = "chandu@example.com" # add
user["city"] = "Bangalore" # update
phone = user.get("phone", "Phone number not available")
print(phone) # Phone number not available
for key, value in user.items():
print(f"{key}: {value}")
# id: 101
# name: Chandu
# age: 34
# city: Bangalore
# email: chandu@example.comimport copy
a = {"x": 1}
b = a # not a copy - the same dictionary
b["y"] = 2
print(a) # {'x': 1, 'y': 2}
employee = {"name": "Ravi", "skills": ["Python"]}
shallow = employee.copy()
shallow["name"] = "Kiran" # own key - original unchanged
shallow["skills"].append("AWS") # shared list - original changed
print(employee) # {'name': 'Ravi', 'skills': ['Python', 'AWS']}
deep = copy.deepcopy(employee)
deep["skills"].append("Go")
print(employee["skills"]) # ['Python', 'AWS'] - untouched this timeDictionary methods
Start with get, keys, values, items, update, and pop. The rest you will meet as you need them.
get(key, default=None)Read a value without risking KeyError.
user.get("email", "Not provided")keys()A view of the keys.
list(user.keys())
values()A view of the values.
list(user.values())
items()A view of (key, value) pairs.
for key, value in user.items():
update(other)Add or overwrite several keys at once.
user.update({"age": 31})pop(key, default)Remove a key and return its value.
user.pop("age")popitem()Remove and return the last pair added.
user.popitem()
setdefault(key, value)Return the value, inserting it first if the key is missing.
user.setdefault("lang", "Python")clear()Remove every key.
user.clear()
copy()A shallow copy - nested values are shared.
snapshot = user.copy()
Operations and built-ins
d[key]Read; KeyError if missing.
user["name"]
d[key] = valueAdd a new key or update an existing one.
user["city"] = "Hyderabad"
del d[key]Remove; KeyError if missing.
del user["city"]
key in dIs this a key? Checks keys only.
"email" in user
len(d)Number of key-value pairs.
len(user)
a | bPython 3.9+: a new merged dictionary, right side wins.
defaults | overrides
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Read user["email"] from a dictionary without that key and read the error message. Then rewrite it with get() and a default.”
“Assign user["age"] = 31 on a dictionary that has age and on one that does not. Print both and describe the difference.”
“With user = {"name": "Ravi"}, compare "name" in user, "Ravi" in user, and "Ravi" in user.values().”
“Write a dictionary literal with the same key twice and print it. Which value survived, and in which position?”
“Copy a dictionary that holds a list, append to the list through the copy, and print the original.”
What usually goes wrong
There are no positions. user[0] looks for a key named 0 and raises KeyError: 0.
✗ print(user[0])✓ print(user["name"])"name" and "Name" are different keys. Python is case-sensitive, so user["Name"] raises KeyError: ‘Name’.
✗ print(user["Name"])✓ print(user["name"])Optional data needs get() or an in check, or the program stops with KeyError.
✗ print(user["email"])✓ print(user.get("email", "Not provided"))The earlier value is lost silently - no error, no warning.
✗ user = {"name": "Ravi", "name": "Kiran"} # {'name': 'Kiran'}✓ users = [{"name": "Ravi"}, {"name": "Kiran"}]Adding or removing keys during the loop raises RuntimeError: dictionary changed size during iteration. Loop over a copy of the keys instead.
✗ for key in user:
if not user[key]:
del user[key]✓ for key in list(user):
if not user[key]:
del user[key]copy() is shallow: lists and dictionaries inside are shared. Use copy.deepcopy() for independent nested data.
✗ backup = settings.copy()✓ import copy
backup = copy.deepcopy(settings)Best practices
- Use a dictionary when fields have names; use a list when position is the meaning.
- Use [] when a key must exist and get() when it is genuinely optional.
- Give get() a default that makes sense to print or compute with, not just None.
- Loop with for key, value in d.items() when you need both.
- Keep keys consistent: one spelling, one case - "email", never "Email" and "e_mail" too.
- Use pop(key, default) or an in check before removing keys that may not exist.
- Never add or remove keys while looping over the same dictionary.
- Reach for copy.deepcopy() when a dictionary holds lists or other dictionaries you will change.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Create student = {"name": "Ravi", "age": 25, "course": "Python"} and print the student’s name.
Show hintHide hint
Square brackets with the key.
Show solutionHide solution
student = {"name": "Ravi", "age": 25, "course": "Python"}
print(student["name"]) # RaviAdd city Hyderabad, change the age from 25 to 26, then check whether "email" exists.
Show hintHide hint
Assignment both adds and updates; in checks keys.
Show solutionHide solution
student = {"name": "Ravi", "age": 25, "course": "Python"}
student["city"] = "Hyderabad"
student["age"] = 26
print(student)
# {'name': 'Ravi', 'age': 26, 'course': 'Python', 'city': 'Hyderabad'}
print("email" in student) # FalseRemove "age", then loop over what is left and print each pair as name = Ravi.
Show hintHide hint
pop() or del, then items() with an f-string.
Show solutionHide solution
student = {"name": "Ravi", "age": 26, "course": "Python", "city": "Hyderabad"}
student.pop("age")
for key, value in student.items():
print(f"{key} = {value}")
# name = Ravi
# course = Python
# city = HyderabadFrom student = {"name": "Ravi", "skills": ["Python", "SQL", "AWS"]}, print "SQL".
Show hintHide hint
Two steps: the skills list, then a position in it.
Show solutionHide solution
student = {"name": "Ravi", "skills": ["Python", "SQL", "AWS"]}
print(student["skills"][1]) # SQLGiven users = [{"name": "Ravi", "age": 30}, {"name": "Kiran", "age": 28}], print each user’s name with a loop.
Show hintHide hint
Each item of the list is a dictionary.
Show solutionHide solution
users = [
{"name": "Ravi", "age": 30},
{"name": "Kiran", "age": 28},
]
for user in users:
print(user["name"])
# Ravi
# KiranEmployee record
Build an employee dictionary with id, name, department, salary and a list of skills, then work with it the way an application would.
- Print the employee’s name.
- Add "Docker" to the skills list.
- Update the salary.
- Add a "city" field.
- Check whether "email" exists, and print a fallback message if it does not.
- Print every key-value pair as key: value.
employee = {
"id": 101,
"name": "Ravi",
"department": "Engineering",
"salary": 80000,
"skills": ["Python", "React", "AWS"],
}
# 1. Print the name
# 2. Add "Docker" to skills
# 3. Update the salary
# 4. Add a city
# 5. Check for email
# 6. Print every key-value pair
Show one solutionHide solution
employee = {
"id": 101,
"name": "Ravi",
"department": "Engineering",
"salary": 80000,
"skills": ["Python", "React", "AWS"],
}
print(employee["name"]) # Ravi
employee["skills"].append("Docker") # the value is a list - append to it
employee["salary"] = 90000
employee["city"] = "Hyderabad"
print(employee.get("email", "Email not available"))
# Email not available
for key, value in employee.items():
print(f"{key}: {value}")
# id: 101
# name: Ravi
# department: Engineering
# salary: 90000
# skills: ['Python', 'React', 'AWS', 'Docker']
# city: HyderabadKey points
- A dictionary stores key-value pairs: names instead of positions.
- {} and dict() are empty dictionaries; the empty set is set().
- d[key] raises KeyError when the key is missing; d.get(key, default) does not.
- d[key] = value adds a new key or updates an existing one.
- pop() removes and returns; del removes; clear() empties.
- key in d checks keys only - use d.values() to search values.
- for key, value in d.items() is the loop you will write most.
- keys(), values() and items() are live views; wrap them in list() for a list.
- Keys are unique and hashable; a repeated key keeps the last value.
- Dictionaries keep insertion order (Python 3.7+).
- copy() is shallow - nested lists are shared; use copy.deepcopy() for independence.
Quick check before you move on
Interview questions
What is a dictionary in Python?
A mutable mapping of unique, hashable keys to values, implemented as a hash table. Lookup, insertion and deletion by key are O(1) on average, and since Python 3.7 it preserves insertion order.
What can be a dictionary key?
Any hashable value: strings, numbers, booleans, tuples of hashables, frozensets. Lists, sets and dictionaries cannot, because they are mutable and their hash could change. Note 1, 1.0 and True are equal and so the same key.
How do you avoid a KeyError?
Check with in, read with get(key, default), remove with pop(key, default), or use setdefault() when you want to insert a default. For nested data chain get() with {} as the default.
Are dictionaries ordered?
Yes, by insertion order, guaranteed since Python 3.7. Updating an existing key keeps its position; deleting and re-adding moves it to the end.
Dictionary or list for a user record?
A dictionary: fields are reached by meaningful names rather than positions, the code documents itself, and adding a field does not shift anything. A list suits an ordered sequence of similar items - often a list of dictionaries.
Quiz
- 1.
When should you use [] and when get()?
- 2.
What is the difference between pop() and del?
- 3.
Why does print(user.keys()) not look like a list?
- 4.
You copy a dictionary with copy() and append to a list inside the copy. What happens to the original?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned