Python Nested Dictionaries

In the last lesson you learned Dictionary Methods, so you can now read, add, and update keys. But a plain dictionary only goes one level deep. Real data is rarely that flat. A user is not just a name. A user has a name, an age, a city, an email, and more. That is where nested dictionaries come in.

πŸ€” Why Nested Dictionaries?

Say you are storing users for an app. With a flat dictionary you would have to flatten everything into separate keys. Here is what that looks like.

This is a flat dictionary trying to hold two users. It gets messy fast.

users = {
"u1_name": "Alex",
"u1_city": "London",
"u2_name": "Riya",
"u2_city": "Mumbai",
}

See the problem? The user id and the field are mashed into one key. There is no clean way to say β€œgive me everything about user u1”. You would have to guess key names and join them together by hand.

A nested dictionary fixes this. You keep one key per user. The value for that user is its own little dictionary of details. One record, one place, neatly grouped.

🧩 What Is a Nested Dictionary?

A nested dictionary is simply a dictionary inside another dictionary. The outer dictionary holds keys like user ids. Each of those keys points to an inner dictionary that holds the details for that one user.

Think of it like a filing cabinet. The cabinet is the outer dictionary. Each drawer has a label, which is the user id. Inside each drawer is a folder with that person’s details, which is the inner dictionary.

Here are the same two users, but done properly with nesting.

users = {
"u1": {"name": "Alex", "city": "London", "age": 28},
"u2": {"name": "Riya", "city": "Mumbai", "age": 24},
}

Now users has just two keys: "u1" and "u2". The value behind each one is a full record. It is clean, grouped, and easy to grow.

πŸ”‘ Reading a Nested Value

To reach a detail deep inside, you use two keys, one after the other. The first key picks the user. The second key picks the field on that user.

This reads the city of user u1 by going one level at a time.

users = {
"u1": {"name": "Alex", "city": "London", "age": 28},
"u2": {"name": "Riya", "city": "Mumbai", "age": 24},
}
print(users["u1"]["city"])
print(users["u2"]["name"])

Output

London
Riya

Read it left to right. users["u1"] gives you the inner dictionary {"name": "Alex", "city": "London", "age": 28}. Then ["city"] reaches into that inner dictionary and pulls out "London". The two keys just walk you down the levels. First the user, then the field.

Caution

If you ask for a key that is not there, like users["u9"]["city"], Python raises a KeyError and the program stops. When you are not sure a key exists, reach for .get() instead. We will use it below.

πŸ›‘οΈ Reading Safely with .get()

A KeyError crashes the program. When the user id might be missing, use .get() so you get a calm fallback instead of a crash.

This looks up a user that does not exist, but stays safe by giving back a default.

users = {
"u1": {"name": "Alex", "city": "London", "age": 28},
}
# ❌ Avoid: this crashes with KeyError if "u9" is missing
# print(users["u9"]["city"])
# βœ… Good: get the inner dict safely, then read from it
user = users.get("u9", {})
print(user.get("city", "Unknown"))

Output

Unknown

The first .get("u9", {}) asks for user u9. Since u9 is not there, it hands back an empty dictionary instead of None. Then .get("city", "Unknown") asks that empty dictionary for a city, finds none, and returns "Unknown". There is no crash. You just get a sensible default.

βž• Adding and Updating Nested Data

Adding a whole new user means setting a new outer key to a fresh inner dictionary. Updating one field means reaching in with two keys and assigning.

This adds a brand new user, then changes a single field on an existing one.

users = {
"u1": {"name": "Alex", "city": "London", "age": 28},
}
# Add a whole new user record
users["u2"] = {"name": "Riya", "city": "Mumbai", "age": 24}
# Update one field on an existing user
users["u1"]["city"] = "Berlin"
# Add a new field to an existing user
users["u1"]["email"] = "alex@example.com"
print(users["u1"])
print(users["u2"])

Output

{'name': 'Alex', 'city': 'Berlin', 'age': 28, 'email': 'alex@example.com'}
{'name': 'Riya', 'city': 'Mumbai', 'age': 24}

Three different moves are happening here, so let us walk through them.

  • users["u2"] = {...} sets a new outer key. The whole inner dictionary becomes the value for u2.
  • users["u1"]["city"] = "Berlin" walks in two levels and overwrites just the city. Alex moved from London to Berlin.
  • users["u1"]["email"] = "..." uses a key that was not there before, so Python adds it as a new field on that record.

The same rule you already know applies at each level. If the key exists you change it. If it does not exist you create it.

πŸ” Looping Over a Nested Dictionary

Most of the time you want to walk every record and do something with it. You loop over the outer dictionary to get each user id and its inner dictionary. Then you read fields from inside.

This loops over every user and prints a short line about each one.

users = {
"u1": {"name": "Alex", "city": "London", "age": 28},
"u2": {"name": "Riya", "city": "Mumbai", "age": 24},
"u3": {"name": "Arjun", "city": "Toronto", "age": 31},
}
for user_id, details in users.items():
name = details["name"]
city = details["city"]
print(f"{user_id}: {name} lives in {city}")

Output

u1: Alex lives in London
u2: Riya lives in Mumbai
u3: Arjun lives in Toronto

Here .items() hands you two things on each turn of the loop. user_id is the outer key, like "u1". details is the inner dictionary for that user. Once you have details, you read its fields the normal way with a single key. At that point you are already one level deep.

You can also do quick maths across records. This adds up the ages of everyone.

users = {
"u1": {"name": "Alex", "age": 28},
"u2": {"name": "Riya", "age": 24},
"u3": {"name": "Arjun", "age": 31},
}
total = 0
for details in users.values():
total += details["age"]
print(f"Total age: {total}")

Output

Total age: 83

When you only need the inner dictionaries and not the ids, loop over users.values(). Each details is one inner record. Then details["age"] pulls the age out so you can add it to the running total.

⚠️ Common Mistakes

A few slip-ups catch people often with nested dictionaries.

  • Reading a missing outer key directly. users["u9"]["city"] crashes with a KeyError when u9 is not there. Use .get() with a default when the key might be missing.
  • Forgetting the second key. users["u1"] gives you the whole inner dictionary, not a single field. You still need ["city"] to reach the value.
  • Mixing up the two levels in a loop.
users = {"u1": {"name": "Alex", "city": "London"}}
# ❌ Avoid: treating the inner dict like a plain value
for user_id in users:
print(users[user_id]) # prints the whole inner dict, not a field
# βœ… Good: step into the inner dict for the field you want
for user_id, details in users.items():
print(details["name"]) # prints just the name

βœ… Best Practices

Keep these habits and nested data stays easy to work with.

  • Keep the inner records the same shape. If every user has name, city, and age, looping and reading stays simple and predictable.
  • Use .get("field", default) when a field might be missing on some records, so one odd record does not crash the whole loop.
  • Use clear, meaningful keys. "u1" or an email reads better than a random number you cannot trace back.
  • Do not nest deeper than you truly need. Two or three levels is fine. Past that, reading and updating gets hard, and a different structure or a class is usually clearer.

🧩 What You’ve Learned

A quick recap of what you can now do.

  • βœ… A nested dictionary is a dictionary inside another dictionary, perfect for holding richer records like users.
  • βœ… Read a deep value with two keys in a row, like users["u1"]["city"], walking one level at a time.
  • βœ… Read safely with .get() and a default so a missing key does not crash your program.
  • βœ… Add a new record by setting a new outer key, and update a field by reaching in with two keys.
  • βœ… Loop with .items() to get each id and its inner dictionary, or .values() when you only need the records.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Given users = {"u1": {"name": "Alex", "city": "London"}}, how do you read Alex's city?

    Why: You use two keys in a row: the first picks the user, the second picks the field on that user.

  2. 2

    What does users.get("u9", {}) return if the key "u9" is not in the dictionary?

    Why: The second argument to .get() is the default, so a missing key returns the empty dictionary you passed in instead of crashing.

  3. 3

    How do you change only the city of an existing user u1 to "Berlin"?

    Why: Reach in with two keys and assign, which overwrites just the city while leaving the rest of the record untouched.

  4. 4

    In for user_id, details in users.items():, what is details on each turn?

    Why: items() gives the outer key and its value, so user_id is the id and details is the whole inner dictionary.

πŸš€ What’s Next?

Now that you can build and read nested records by hand, the next step is creating dictionaries quickly in one clean line.

Dictionary Comprehensions

Share & Connect