DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Working with Nested Lists and Dictionaries

Real-world data is rarely as flat as the simple examples earlier in this series. python nested dictionaries and python nested lists — structures containing other structures — are the actual shape of almost everything you'll work with once you start pulling in API responses, JSON files, or configuration data. This article covers building, accessing, modifying, and safely navigating that kind of layered data.

Introduction: Data Structures Inside Data Structures

Nesting simply means using a list or dictionary as an element or value inside another list or dictionary.

nested = {"user": {"name": "Alex", "roles": ["admin", "editor"]}}

That single line already contains three levels: an outer dictionary, an inner dictionary as one of its values, and a list nested inside that inner dictionary.

Why this matters

Real-world data — API responses, parsed JSON, configuration files — is almost never flat. A user profile has an address, which has its own fields. An order has a list of line items, each of which is its own record with several fields. Learning to navigate this kind of layered structure isn't an advanced, rarely-needed skill — it's close to unavoidable the moment you start working with data from outside your own program.

Four combinations, covered in this article
  • List of lists — a grid or matrix-like structure.

  • List of dictionaries — a common shape for records, like rows from a database or API.

  • Dictionary of dictionaries — records keyed by some identifier, each holding several fields.

  • Dictionary of lists — a single key mapping to multiple related values.

Creating and Accessing Nested Structures

Building a dictionary of dictionaries
students = {
    "Alex": {"id": 101, "email": "[email protected]", "grade": "A"},
    "Sam": {"id": 102, "email": "[email protected]", "grade": "B"},
}

Here, each student's name is the outer key, and the value is itself a dictionary holding that student's individual fields.

Building a list of dictionaries
students = [
    {"name": "Alex", "id": 101, "grade": "A"},
    {"name": "Sam", "id": 102, "grade": "B"},
]

This shape — a list of dictionaries — is extremely common in practice, since it's exactly what you get back from most API endpoints returning a collection of records, and it's the direct equivalent of rows in a database table.

A dictionary containing lists
student_hobbies = {
    "Alex": ["reading", "hiking", "chess"],
    "Sam": ["painting", "cycling"],
}

Here, each key maps to a list of multiple related values, rather than a single dictionary of fields.

Chaining lookups to reach a deeply nested value

Accessing a deeply nested value just means chaining bracket lookups, one level at a time:

students = {
    "Alex": {"id": 101, "email": "[email protected]", "grade": "A"},
}

print(students["Alex"]["email"])   # [email protected]

With a list of dictionaries, you combine index-based access with key-based access:

students = [
    {"name": "Alex", "id": 101, "grade": "A"},
    {"name": "Sam", "id": 102, "grade": "B"},
]

print(students[0]["name"])   # Alex — first list item, then its "name" key
Each level must be indexed in the right order

This is worth stating plainly: you need to know whether a given level is a list (indexed by position, [0]) or a dictionary (indexed by key, ["email"]) at each step, and access it accordingly. Mixing these up — trying to use a string key on a list, or a numeric index on a dictionary — raises an error immediately:

students = [{"name": "Alex"}]

print(students["name"])
# TypeError: list indices must be integers or slices, not str

Modifying Nested Data

Adding a new key to an inner dictionary
students = {"Alex": {"id": 101, "grade": "A"}}

students["Alex"]["email"] = "[email protected]"
print(students)
# {'Alex': {'id': 101, 'grade': 'A', 'email': '[email protected]'}}
Adding a new nested entry to the outer structure
students["Sam"] = {"id": 102, "grade": "B"}
print(students)
# {'Alex': {...}, 'Sam': {'id': 102, 'grade': 'B'}}
Updating a value several levels deep
students = {"Alex": {"id": 101, "email": "[email protected]", "grade": "A"}}

students["Alex"]["email"] = "[email protected]"
print(students["Alex"]["email"])   # [email protected]

Each bracket access along the chain navigates one level deeper, and the final bracket is what actually gets assigned to.

Appending to a list nested inside a dictionary value
student_hobbies = {"Alex": ["reading", "hiking"]}

student_hobbies["Alex"].append("chess")
print(student_hobbies)   # {'Alex': ['reading', 'hiking', 'chess']}

This works exactly like appending to any other list — you just need to navigate to it first via the dictionary key.

Safely Accessing Uncertain or Deeply Nested Data

The problem: real data isn't uniformly shaped

Data coming from outside your program — an API, a file, user input — doesn't always guarantee every key will be present, or that every value will be the type you expect. Chained bracket access has no tolerance for this:

data = {"user": {"name": "Alex"}}

print(data["user"]["email"])
# KeyError: 'email' — this key simply doesn't exist for this record

If even one key is missing anywhere along the chain, your program crashes immediately with a KeyError (or a TypeError, if a level you expected to be a dict turns out to be something else, like None).

Safer access with chained .get() and defaults

The .get() method, covered in the earlier dictionaries article, extends naturally to nested access — you just chain it, providing a fallback at each level:

data = {"user": {"name": "Alex"}}

email = data.get("user", {}).get("email", "No email provided")
print(email)   # No email provided

Here, data.get("user", {}) returns the inner dictionary if "user" exists, or an empty dictionary if it doesn't — either way, the next .get("email", ...) call always has a dictionary to operate on, so it never raises an error, regardless of how incomplete the source data turns out to be.

When to let it fail loudly instead

Safe access isn't always the right call. If a missing key genuinely represents a bug — data that should always be present, and its absence signals something is actually broken upstream — deliberately letting a KeyError happen (rather than silently substituting a default) is often the more honest choice. Swallowing every possible missing key with a default can hide real problems instead of surfacing them where they're easiest to diagnose. Use .get() with defaults for data that's legitimately optional; use direct bracket access (and let it raise) for data you genuinely expect to always be there.

A small reusable helper for dynamic key paths

For situations where you need to look up a nested value using a path determined at runtime — not hardcoded key names — a small helper function is worth having on hand:

def get_nested(data, keys, default=None):
    """Safely access a nested value using a list of keys."""
    current = data
    for key in keys:
        if isinstance(current, dict) and key in current:
            current = current[key]
        else:
            return default
    return current

data = {"user": {"address": {"city": "Nagpur"}}}

print(get_nested(data, ["user", "address", "city"]))       # Nagpur
print(get_nested(data, ["user", "address", "zip"]))        # None
print(get_nested(data, ["user", "phone", "number"], "N/A")) # N/A

This is particularly useful when the key path itself is configurable or comes from somewhere dynamic — for example, extracting different fields from a JSON response based on a mapping defined elsewhere in your program.

Looping, Comprehensions, and Practical Patterns

Iterating with nested for loops

The natural pattern for processing nested data is an outer loop over the outer structure, with an inner loop handling whatever's nested inside each item:

students = {
    "Alex": {"id": 101, "grade": "A"},
    "Sam": {"id": 102, "grade": "B"},
}

for name, info in students.items():
    print(f"{name}:")
    for field, value in info.items():
        print(f"  {field}: {value}")
Flattening nested data into a simpler structure

Sometimes you don't need the full nested shape — you just need to extract a specific, simpler view out of it. Here's an example pulling city-population pairs out of a three-level state → city → population structure:

populati {
    "California": {"Los Angeles": 3900000, "San Diego": 1400000},
    "Texas": {"Houston": 2300000, "Austin": 960000},
}

city_populati {}
for state, cities in population_data.items():
    for city, population in cities.items():
        city_populations[city] = population

print(city_populations)
# {'Los Angeles': 3900000, 'San Diego': 1400000, 'Houston': 2300000, 'Austin': 960000}

This same flattening pattern can be written more compactly as a nested dict comprehension, echoing the earlier dict/set comprehension article:

city_populati {
    city: population
    for cities in population_data.values()
    for city, population in cities.items()
}
Real-world framing: this is what parsed JSON looks like

If you've ever looked at a JSON response from a web API, this exact shape — dictionaries containing lists, lists containing dictionaries, several layers deep — is almost always what you're looking at, once Python parses it. The json module (covered in a later article in this series) converts JSON directly into this same combination of nested dicts and lists, so every pattern covered in this article — chained access, safe .get() navigation, nested loops — transfers directly to working with real API data, with no additional concepts required.

A caution about over-nesting

While nesting is often unavoidable when it reflects the genuine shape of your data, it's worth watching for the point where it becomes a readability problem rather than a natural representation. If you find yourself writing chains like data["a"]["b"]["c"]["d"]["e"], or nesting comprehensions three or four levels deep, that's usually a sign to step back and reconsider the structure. Beyond roughly two or three levels, it's often worth introducing a dataclass (or a full class) to give each level a named, well-defined shape, rather than continuing to nest plain dictionaries and lists indefinitely. Dictionaries and lists are flexible and convenient for genuinely dynamic or loosely structured data — but once a structure's shape is stable and well-known ahead of time, a proper class often makes the code considerably easier to read, and gives you the benefit of catching typos in field names as an actual error, rather than a silently returned None.

PREVIOUSNEXT LESSON