← Back to Core Python
Lesson 19 · Python Collections

Dictionaries

Data with names instead of positions: create, read safely, update, remove, and loop over key-value pairs.

Beginner45 min

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.

Ways to create one
{}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", "age": 30}
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.

Removing
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.

Three views of user = {"name": "Ravi", "age": 30, "city": "Hyderabad"}
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().

Choosing the collection
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

Creating and reading
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}
Missing keys: [] raises, get() does not
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 available
Adding, updating, removing
user = {"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) # {}
Keys, values, items, and looping
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 Hyderabad
Duplicate keys and odd keys
print({"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'
Nested data - the shape of JSON
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 # Kiran
The worked example - a user profile
user = {"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.com
Copies: alias, shallow, deep
import 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 time

Dictionary 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] = value

Add a new key or update an existing one.

user["city"] = "Hyderabad"
del d[key]

Remove; KeyError if missing.

del user["city"]
key in d

Is this a key? Checks keys only.

"email" in user
len(d)

Number of key-value pairs.

len(user)
a | b

Python 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.

Break it on purpose

“Read user["email"] from a dictionary without that key and read the error message. Then rewrite it with get() and a default.”

One syntax, two jobs

“Assign user["age"] = 31 on a dictionary that has age and on one that does not. Print both and describe the difference.”

in checks keys

“With user = {"name": "Ravi"}, compare "name" in user, "Ravi" in user, and "Ravi" in user.values().”

Duplicate keys

“Write a dictionary literal with the same key twice and print it. Which value survived, and in which position?”

Shallow copy

“Copy a dictionary that holds a list, append to the list through the copy, and print the original.”

What usually goes wrong

Indexing a dictionary by position

There are no positions. user[0] looks for a key named 0 and raises KeyError: 0.

✗ print(user[0])
✓ print(user["name"])
Wrong case in a key

"name" and "Name" are different keys. Python is case-sensitive, so user["Name"] raises KeyError: ‘Name’.

✗ print(user["Name"])
✓ print(user["name"])
[] on a key that may be missing

Optional data needs get() or an in check, or the program stops with KeyError.

✗ print(user["email"])
✓ print(user.get("email", "Not provided"))
Writing a key twice

The earlier value is lost silently - no error, no warning.

✗ user = {"name": "Ravi", "name": "Kiran"}   # {'name': 'Kiran'}
✓ users = [{"name": "Ravi"}, {"name": "Kiran"}]
Changing a dictionary while looping over it

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]
Thinking copy() copies nested data

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.

1.

Create student = {"name": "Ravi", "age": 25, "course": "Python"} and print the student’s name.

Show hint

Square brackets with the key.

Show solution
student = {"name": "Ravi", "age": 25, "course": "Python"} print(student["name"]) # Ravi
2.

Add city Hyderabad, change the age from 25 to 26, then check whether "email" exists.

Show hint

Assignment both adds and updates; in checks keys.

Show 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) # False
3.

Remove "age", then loop over what is left and print each pair as name = Ravi.

Show hint

pop() or del, then items() with an f-string.

Show 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 = Hyderabad
4.

From student = {"name": "Ravi", "skills": ["Python", "SQL", "AWS"]}, print "SQL".

Show hint

Two steps: the skills list, then a position in it.

Show solution
student = {"name": "Ravi", "skills": ["Python", "SQL", "AWS"]} print(student["skills"][1]) # SQL
5.

Given users = [{"name": "Ravi", "age": 30}, {"name": "Kiran", "age": 28}], print each user’s name with a loop.

Show hint

Each item of the list is a dictionary.

Show solution
users = [ {"name": "Ravi", "age": 30}, {"name": "Kiran", "age": 28}, ] for user in users: print(user["name"]) # Ravi # Kiran
Coding challenge

Employee record

Build an employee dictionary with id, name, department, salary and a list of skills, then work with it the way an application would.

It should
  • 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 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: Hyderabad

Key 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

user = {"name": "Ravi", "age": 30}. What does user.get("city", "Unknown") return, and what is len(user) afterwards?
‘Unknown’, and still 2 - get() reads, it never adds the key.
What does user[0] do on a dictionary?
Raises KeyError: 0. Dictionaries have no positions, so 0 is looked up as a key.
user["age"] = 31 - does it add or update?
Either: it updates "age" if the key exists and adds it if it does not.
Is "Ravi" in {"name": "Ravi"} True?
False. in checks keys; "Ravi" is a value. "Ravi" in user.values() is True.
What does {"name": "Ravi", "name": "Chandu"} contain?
One key: {‘name’: ‘Chandu’}. The last value wins, without a warning.
How do you read a city from user = {"address": {"city": "Hyderabad"}}?
user["address"]["city"] - the address dictionary first, then its city.

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. 1.

    When should you use [] and when get()?

  2. 2.

    What is the difference between pop() and del?

  3. 3.

    Why does print(user.keys()) not look like a list?

  4. 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...