Short answer
- In Python, an object inside another object means a value that is itself an object stored as part of a larger object. The two common forms are data nesting, such as a dictionary inside a dictionary or a list inside a list, and composition, where one class instance is stored as an attribute of another class instance.
- You build a nested dictionary by using a dictionary as a value:
catalog = {"fiction": {"dune": 1965}}. You read a value two levels down with chained keys:catalog["fiction"]["dune"]gives1965. - You store an object as an attribute by assigning it in
__init__, for exampleself.engine = engineinside aCarclass. You then reach it with a dot chain:car.engine.horsepower. - Accessing nested data safely matters more than building it. A missing key raises
KeyError, and a missing attribute raisesAttributeError, so use.get()with a default or a small helper function when a level might be absent. - Nesting helps when the inner object belongs to the outer one and the structure mirrors the real thing, such as an address belonging to a customer. Nesting hurts when chains grow long, when the same inner object is shared and changed from two places, or when a flat structure would be easier to search and test.
What “An Object Inside An Object” Means
Python has a simple rule at the heart of this topic: everything in Python is an object, and an object can hold other objects. The Python documentation puts it plainly in its classes tutorial: creating a new class creates a new type of object, and each class instance can have attributes attached to it for maintaining its state. Those attributes can be numbers, strings, lists, dictionaries, or other class instances. When an attribute is itself an object, you have an object inside an object.
There are two distinct patterns hiding inside that one phrase, and mixing them up is the most common source of confusion for learners. The first is data nesting, where a built-in container holds another container: a dictionary inside a dictionary, a list inside a list, or a dictionary inside a list. The second is composition, where a class you defined holds an instance of another class you defined as one of its attributes. A car that has an engine is composition. A catalog dictionary that holds a dictionary of books per shelf is data nesting. Both are objects inside objects, but they are built differently, read differently, and fail differently, so this article treats them separately before bringing them together in a complete example.
A third idea sits underneath both patterns and is worth stating early: Python stores references, not copies. When you put an object inside another object, the outer object holds a reference to the inner object, not a separate duplicate of it. That is why changing the inner object through one name can be visible through another name, a behavior this article works through in the section on copying. It is also why passing an object into a function is cheap: the Python documentation notes that passing an object passes only a pointer, and if a function modifies the object passed as an argument, the caller sees the change.
Dictionaries Inside Dictionaries
A nested dictionary is the most common form of an object inside an object, and it is the form most beginners meet first. A dictionary maps keys to values, and nothing in Python stops a value from being another dictionary. That makes nested dictionaries a natural fit for data that has levels: a library catalog grouped by section, a settings file grouped by feature, or an API response grouped by resource.
Here is a small catalog, with books grouped by section:
catalog = {
"fiction": {"dune": 1965, "neuromancer": 1984},
"science": {"a-brief-history-of-time": 1988},
}
Reading a nested value uses chained square brackets, one pair per level:
print(catalog["fiction"]["dune"])
This prints 1965. Python reads the expression from left to right: catalog["fiction"] returns the inner dictionary {"dune": 1965, "neuromancer": 1984}, and then ["dune"] looks up the year inside that dictionary. Each bracket peels off one level, and you can chain as many levels as the data has.
Writing to a nested dictionary works the same way, but it has a trap that catches many learners. Assigning to an existing path works directly:
catalog["fiction"]["dune"] = 1966
That changes the year stored under dune. But creating a new nested path in one step does not work. If you write catalog["history"]["sapiens"] = 2011 and there is no "history" key yet, Python raises KeyError, because it cannot look up a key inside a dictionary that does not exist. You must create the inner dictionary first:
catalog["history"] = {}
catalog["history"]["sapiens"] = 2011
The dict.setdefault() method is a cleaner way to do the same thing in one line. It returns the value for a key if the key exists, and if it does not, it inserts a default value and returns that:
catalog.setdefault("history", {})["sapiens"] = 2011
For data with many levels, the collections.defaultdict type can create inner dictionaries automatically, but for most everyday code, setdefault() is enough, and it keeps the structure visible.
Nested dictionaries are also how JSON data usually arrives in Python, and that connection matters in practice. When you load a JSON document with json.loads(), nested JSON objects become nested Python dictionaries, so the same chained access works on data from a web API. Our guide to what JSON encode means explains the JSON side of that exchange in more detail, and the JSON section later in this article shows a full round trip.
Lists Inside Lists
Lists nest just as freely as dictionaries, and the classic example is a grid: a list of rows, where each row is itself a list. This is how a spreadsheet, a game board, or a small table is usually represented in plain Python.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Access uses chained indexes, in the same left to right order as dictionaries:
print(matrix[1][2])
This prints 6. matrix[1] is the second row, [4, 5, 6], and [2] takes the third item in that row. Counting starts at zero, so row 1 is the second row and index 2 is the third column.
A list comprehension can pull one column out of every row, which is a common operation on nested lists:
print([row[0] for row in matrix])
This prints [1, 4, 7], the first item of each row. The Python documentation uses exactly this matrix shape in its data structures tutorial, where a nested list comprehension transposes rows and columns, turning [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]] into [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]].
There is one list nesting mistake that deserves a warning of its own. Multiplying a list of lists does not copy the inner lists:
grid = [[0] * 3] * 3
grid[0][0] = 1
After those lines, grid is [[1, 0, 0], [1, 0, 0], [1, 0, 0]], because the outer multiplication copied the reference to the same inner list three times, so all three rows are the same object. Changing one row changes all three. The fix is a list comprehension that builds a fresh inner list for each row:
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
Now grid is [[1, 0, 0], [0, 0, 0], [0, 0, 0]], because each row is a separate list. This is the same reference behavior that appears later in the copying section, and it is the single most surprising fact about nested containers for new Python programmers.
Objects as Attributes: Composition
Data nesting covers dictionaries and lists. Composition covers classes, and it is the pattern professional Python code uses most. Composition means a class holds an instance of another class as an attribute, because the outer object has an inner object that does part of its work. The textbook example is a car and its engine: a car has an engine, and the engine knows how to start itself.
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return f"Engine with {self.horsepower} hp started"
class Car:
def __init__(self, model, engine):
self.model = model
self.engine = engine
def start(self):
return f"{self.model}: {self.engine.start()}"
The Car class does not build its own engine. It receives an engine object through its constructor and stores it as self.engine. That one line, self.engine = engine, is the object inside the object. Building and using the pair looks like this:
car = Car("Falcon", Engine(180))
print(car.start())
print(car.engine.horsepower)
The output is:
Falcon: Engine with 180 hp started
180
Two things are worth noticing. First, car.start() delegates to the engine: the car asks its inner object to do the engine work, rather than doing that work itself. That delegation is the point of composition. Each class stays small and focused, and the outer class coordinates the inner ones. Second, the dot chain car.engine.horsepower reaches through two objects: car.engine is the engine object, and .horsepower is an attribute of that engine. Dot chains read left to right, just like chained brackets on dictionaries.
Passing the inner object into the constructor, as this example does, is generally better than building it inside __init__. When the caller supplies the engine, the same Car class can hold a small engine or a large one, and a test can supply a fake engine that records calls instead of starting a real one. This pattern is sometimes called dependency injection, but the plain description is enough: give the outer object its inner object, rather than making the outer object build it.
Composition is also how most real Python programs are structured. A web request object holds a user object, a user object holds an address object, and an order holds a customer. The complete worked example at the end of this article builds exactly that chain, and it shows how far a few small classes can go.
A related distinction is worth a short paragraph: composition is a has-a relationship, and it is different from inheritance, which is an is-a relationship. A car has an engine, so the engine is an attribute. A sports car is a car, so the sports car is a subclass. Reaching for inheritance when composition fits is a classic design mistake, because a subclass inherits everything, including behavior it does not want, while composition lets the outer class expose only the inner behavior it chooses.
Accessing Nested Data Safely
Building nested objects is the easy half of the topic. Reading them without crashing is the hard half, because every level of nesting is a chance for a missing key or a missing attribute to stop the program.
Direct chained access is fine when you control the data and know every level exists:
user = {"name": "Priya", "address": {"city": "Austin"}}
print(user["address"]["city"])
This prints Austin. But if the "address" key is missing, the same expression raises KeyError at the first bracket, and the program stops. The .get() method returns a default instead of raising an error, and it chains nicely:
print(user.get("address", {}).get("zip", "unknown"))
This prints unknown. user.get("address", {}) returns the address dictionary, or an empty dictionary if the key is missing, and .get("zip", "unknown") then returns "unknown" because there is no zip code stored. The trick is the {} default: it guarantees the second .get() always has a dictionary to call on, even when the address is absent entirely.
When chains get longer than two levels, a small helper function is cleaner than a long string of .get() calls:
def get_nested(data, *keys, default=None):
for key in keys:
if isinstance(data, dict):
data = data.get(key, default)
else:
return default
return data
profile = {"name": "Priya", "settings": {"theme": "dark", "notifications": {"email": True}}}
print(get_nested(profile, "settings", "notifications", "email"))
print(get_nested(profile, "settings", "language", default="English"))
print(get_nested(profile, "address", "zip", default="unknown"))
The output is:
True
English
unknown
The helper walks down one key at a time, returns the default as soon as a level is missing or is not a dictionary, and returns the value found at the end of the chain. It is short, reusable, and it turns a crash into a predictable default, which is usually what a program wants when it reads optional settings or partial API data.
For objects rather than dictionaries, the equivalent tool is getattr(), which reads an attribute by name and returns a default when it is missing: getattr(car, "engine", None). Chaining getattr() calls is awkward, so for deep object chains most programmers either check each level with an if statement or use a small helper like the one above, adapted for attributes. Python 3.10 and later also offer the match statement for branching on nested structure, but for simple lookups the helper function is the clearest tool.
One more access pattern is worth knowing: unpacking. If a nested structure has a fixed shape, you can pull its parts out in one assignment:
book = {"title": "Dune", "author": {"name": "Frank Herbert", "born": 1920}}
title = book["title"]
author_name = book["author"]["name"]
Assigning the inner values to named variables at the top of a function keeps the rest of the code readable, because the rest of the code uses author_name instead of repeating book["author"]["name"] on every line. Long chains repeated through a function are a sign that the code should either unpack once or restructure the data.
Nested Dataclasses
Python’s dataclasses module, in the standard library, is a convenient way to build nested objects without writing __init__ methods by hand. A dataclass is a class that mainly holds data, and the decorator writes the constructor, the readable representation, and the equality check for you. Dataclasses nest naturally: one dataclass can hold another dataclass as a field.
from dataclasses import dataclass
@dataclass
class Address:
street: str
city: str
@dataclass
class Customer:
name: str
address: Address
customer = Customer("Priya", Address("12 Oak Street", "Austin"))
print(customer.address.city)
print(customer)
The output is:
Austin
Customer(name='Priya', address=Address(street='12 Oak Street', city='Austin'))
The printed form shows the nesting clearly: the customer holds an address object, and the address holds its own fields. Dataclasses are popular for configuration, for API data, and for any structure that is mostly data with a little behavior, because they remove the repetitive constructor code while keeping the object inside object structure explicit.
Dataclasses have one nesting rule that catches people out. A field with a mutable default value, such as a list, cannot be given directly, because all instances would share one list. The field() function with default_factory builds a fresh list for each instance:
from dataclasses import dataclass, field
@dataclass
class Playlist:
name: str
songs: list = field(default_factory=list)
a = Playlist("Morning")
b = Playlist("Evening")
a.songs.append("Song One")
print(a.songs)
print(b.songs)
The output is:
['Song One']
[]
Adding a song to playlist a leaves playlist b empty, because each instance got its own list. A plain list default is not allowed at all: @dataclass rejects songs: list = [] with ValueError: mutable default <class 'list'> for field songs is not allowed: use default_factory. The sharing it guards against is what would happen in a plain class, where two instances would share one list, which is why the decorator refuses it. It is the same shared-reference problem as the grid example earlier, in a different costume, and it is the reason the dataclasses documentation insists on default_factory for mutable defaults.
Nested Objects and JSON
Nested Python objects and JSON fit together closely, because JSON itself is nested: a JSON object can contain other JSON objects, and a JSON array can contain other arrays. When Python reads JSON, nested JSON objects become nested dictionaries, and nested JSON arrays become nested lists. When Python writes JSON, the conversion runs in reverse.
import json
nested = {"book": {"title": "Dune", "author": {"name": "Frank Herbert", "born": 1920}}}
text = json.dumps(nested)
print(text)
print(json.loads(text)["book"]["author"]["name"])
The output is:
{"book": {"title": "Dune", "author": {"name": "Frank Herbert", "born": 1920}}}
Frank Herbert
json.dumps() turns the nested dictionary into a JSON string, and json.loads() turns the string back into nested dictionaries, where the same chained access works. This round trip is how nested data travels between a Python program and a file, a database, or a web API, and it is the reason nested dictionaries are so common in real code: they are the natural shape of data that arrives as JSON.
There is one limit to know. json.dumps() can only handle data made of dictionaries, lists, tuples (serialized as arrays), strings, numbers, booleans, and None. A custom class instance, such as an Engine or a Customer, cannot be converted directly. For those, programmers usually add a method that turns the object into a dictionary, or they use dataclasses with the dataclasses.asdict() helper, which converts a nested dataclass structure into nested dictionaries in one call. The plain dictionaries in the examples above convert with no extra work, which is another reason data nesting is the default choice when data needs to leave the program.
Shared References: Shallow Copy Versus Deep Copy
The most important fact about objects inside objects is also the easiest to overlook: the outer object holds a reference to the inner object, not a copy of it. Two names can point at the same inner object, and changing it through one name changes what the other name sees. That is usually what you want, until it is not, and then you need to copy.
The copy module in the standard library provides two kinds of copy, and the Python documentation draws the distinction in terms of compound objects, meaning objects that contain other objects. A shallow copy constructs a new outer object and inserts references to the same inner objects. A deep copy constructs a new outer object and then recursively inserts copies of the inner objects. The difference only matters for nested structures, which is exactly this article’s subject.
import copy
original = {"user": {"name": "Priya", "tags": ["reader"]}}
shallow = copy.copy(original)
deep = copy.deepcopy(original)
shallow["user"]["tags"].append("writer")
print(original["user"]["tags"])
print(deep["user"]["tags"])
The output is:
['reader', 'writer']
['reader']
The shallow copy shares the inner dictionary with the original, so appending "writer" through the shallow copy also changes the original. The deep copy has its own inner dictionary and its own list, so it is unaffected. The rule is simple: a shallow copy protects the outer level only, and a deep copy protects every level.
Which one to use depends on what you need. A shallow copy is faster and is the right choice when you want a new outer container but are happy to share the inner objects, for example when adding a top level key to a configuration dictionary without disturbing the original. A deep copy is the right choice when you need a fully independent duplicate, for example before modifying a nested structure that another part of the program still uses. Deep copies cost more time and memory, and the documentation notes that they can copy more than intended when data is meant to be shared, so use them deliberately rather than by default.
A related trap appears in function definitions. A mutable default argument, such as a list, is created once when the function is defined, and it is shared by every call that does not supply that argument:
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("a"))
print(add_tag("b"))
The output is:
['a']
['a', 'b']
The second call returns ['a', 'b'], not ['b'], because both calls appended to the same list object. The standard fix is to use None as the default and create a fresh list inside the function. This trap is a nested object problem in disguise: the default list is an object stored inside the function object, and it persists between calls.
When Nesting Helps and When It Hurts
Nesting is a design choice, not a goal in itself, and it pays to ask whether each level earns its place. Nesting helps in three situations. First, when the inner object belongs to the outer one and has no meaning on its own: an address belongs to a customer, an engine belongs to a car, and a chapter belongs to a book. Second, when the structure mirrors the real thing the program models, so a reader can guess the shape of the data from the shape of the world. Third, when the nested form keeps related data together so a function can receive one object instead of a long list of separate arguments.
Nesting hurts in three situations as well. First, when chains grow long. A chain like order.customer.address.city is readable, but a chain four or five levels deep usually means the code is reaching through objects it should not know about, and a method on the outer object would be cleaner. Second, when the same inner object is shared and changed from two places, which produces the aliasing surprises shown in the copying section. Third, when a flat structure would be easier to search, sort, or test. A list of flat records is easier to filter than a deeply nested tree, and a database table is easier to query than a nested dictionary stored as text. If you find yourself writing helper functions just to reach your own data, the data is probably nested too deeply.
A practical guideline is to keep nesting to two or three levels for data you read often, and to give the outer object methods that answer questions about its inner objects, instead of exposing long chains to the rest of the program. A Customer class with a city() method, or an Order class with a ship_to() method, hides the nesting behind a readable name, and the rest of the program never needs to know how deep the structure goes. That is the difference between nesting as a tool and nesting as a tangle.
A Complete Worked Example: Order, Customer, Address
The patterns in this article come together in one small model of an online order. An order has a customer, a customer has an address, and the order can answer the question of where it ships without exposing the chain to its caller.
from dataclasses import dataclass
@dataclass
class Address:
street: str
city: str
@dataclass
class Customer:
name: str
address: Address
class Order:
def __init__(self, order_id, customer):
self.order_id = order_id
self.customer = customer
def ship_to(self):
return f"Order {self.order_id} ships to {self.customer.address.city}"
order = Order(1042, Customer("Priya", Address("12 Oak Street", "Austin")))
print(order.ship_to())
The output is:
Order 1042 ships to Austin
Three levels of nesting are at work here, and each one earns its place. The address belongs to the customer, the customer belongs to the order, and the ship_to() method hides the chain so the code that uses an order never writes order.customer.address.city itself. If the address structure changes later, for example by adding a postal code, only the Order class and the Address class change, and the callers stay the same. That is the payoff of composition: the nesting is an internal detail, and the public behavior is a single readable method.
If you are learning Python and want the wider context, our guide to programming for children covers how programming concepts are introduced to new learners, and our explainer on how software works describes how programs like this one run on a computer. For the people who build software professionally, see our guide to what software developers do. If you are comparing Python with other languages, note that a companion article in this wave compares Java and JavaScript, and another explains what coding is in general.
Common Mistakes With Nested Objects
Most errors with nested objects fall into four groups, and each one has a simple fix.
KeyError is the most common. It means a dictionary key is missing at some level of the chain. The fix is .get() with a default, or a check with in before accessing: if "address" in user. When a program reads data it did not create, such as an API response, assume keys can be missing and code accordingly.
AttributeError is the object version of the same mistake. It means an object does not have the attribute you asked for, usually because the inner object is None or because the attribute was never set. The fix is to check for None before chaining, or to use getattr() with a default, or to set all attributes in __init__ so an object is never half built.
TypeError: 'NoneType' object is not subscriptable appears when a chain returns None partway through, often from a .get() call with no default, and the next bracket tries to index None. The fix is the same as for KeyError: give .get() a sensible default, usually {} for a dictionary level, so the chain never lands on None.
Aliasing is the subtlest mistake of the four, because it does not raise an error at all. Two names point at the same inner object, a change through one name appears through the other, and the program behaves in a way that looks like a bug. The fix is to know when you hold a reference and when you hold a copy, and to use copy.deepcopy() when you need a truly independent duplicate.
A final habit helps with all four: when a nested access fails, print the object one level at a time. print(user), then print(user["address"]), then print(user["address"]["city"]). The level that fails tells you exactly which assumption was wrong, and it is faster than guessing.
FAQ
What does “python object in object” mean?
It means an object stored inside another object. The usual forms are a dictionary inside a dictionary, a list inside a list, or a class instance stored as an attribute of another class instance. The outer object holds a reference to the inner object, so the two are connected rather than copied.
How do you access a nested dictionary in Python?
Use chained square brackets, one pair per level: catalog["fiction"]["dune"]. Python evaluates the chain left to right, returning the inner dictionary at each step and then looking up the next key inside it. For data that might be missing a level, use .get() with a default, such as catalog.get("fiction", {}).get("dune", "unknown"), so a missing key returns the default instead of raising KeyError.
How do you put an object inside another object in Python?
Assign it as an attribute, usually in __init__: self.engine = engine. The inner object can be passed into the constructor by the caller, or created inside __init__. Once stored, you reach it with a dot chain, such as car.engine.horsepower, and you can call its methods, such as car.engine.start().
What is the difference between composition and inheritance in Python?
Composition is a has-a relationship: a car has an engine, so the engine is stored as an attribute. Inheritance is an is-a relationship: a sports car is a car, so it is defined as a subclass. Composition is preferred when the outer object only needs some of the inner object’s behavior, because it keeps each class small and lets the outer class choose exactly what to expose.
Why does changing a nested list change another variable too?
Because Python stores references, not copies. When two names point at the same inner list, a change made through one name is visible through the other. This happens with list multiplication such as [[0] * 3] * 3, with shallow copies, and with mutable default arguments. Use a list comprehension to build separate inner lists, or copy.deepcopy() when you need a fully independent duplicate.
How deep should nesting go in Python?
Two or three levels is a good working limit for data you read often. Beyond that, access chains get hard to read, errors get harder to trace, and a flat structure or a method on the outer object is usually cleaner. If the same chain appears in several places, give the outer object a method that answers the question, such as order.ship_to(), so callers never see the chain at all.
Sources & references
- Python Software Foundation, Python Tutorial, "9. Classes", docs.python.org/3/tutorial/classes.html. Classes bundle data and functionality; class instances can have attributes attached to them; classes themselves are objects; aliasing and the behavior of passing objects to functions. Read 11 November 2026.
- Python Software Foundation, Python Tutorial, "5. Data Structures", docs.python.org/3/tutorial/datastructures.html. List methods, list comprehensions, nested list comprehensions and the matrix transpose example, and dictionary basics. Read 11 November 2026.
- Python Software Foundation, Python Library Reference, "copy: Shallow and deep copy operations", docs.python.org/3/library/copy.html. Assignment creates bindings rather than copies; the difference between shallow and deep copying for compound objects; recursive objects and shared data. Read 11 November 2026.
- Python Software Foundation, Python Library Reference, "dataclasses: Data Classes", docs.python.org/3/library/dataclasses.html. Dataclasses as classes that mainly hold data;
field(default_factory=...)for mutable default values. Read 11 November 2026 for the ledger; cited for thedefault_factoryrule used in the dataclass section. - Python Software Foundation, Python Library Reference, "json: JSON encoder and decoder", docs.python.org/3/library/json.html.
json.dumps()andjson.loads()conversion between Python objects and JSON text. Read 11 November 2026 for the ledger; cited for the JSON round trip example. - Python Software Foundation, Python Library Reference, "Built-in Types", docs.python.org/3/library/stdtypes.html. Mapping types:
dict.setdefault()returns the existing value for a key, or inserts the default and returns it. Read 12 November 2026. - Python Software Foundation, PEP 634, "Structural Pattern Matching: Specification", peps.python.org/pep-0634/. The
matchstatement, introduced in Python 3.10, for branching on nested structure. Read 12 November 2026.