What You’ll Learn
By the end of this lesson, you will understand how Python handles object references and how to choose between assignment, a shallow copy, and a deep copy when working with nested configuration data.
- Recognize why assigning a dictionary does not create a new object.
- Understand which nested values a shallow copy still shares.
- Use
copy.copy()andcopy.deepcopy()appropriately. - Prevent one configuration object from changing another configuration object unexpectedly.
The Concept
In Python, variables hold references to objects. When you assign one variable to another, Python does not create a second object:
production_config = {"debug": False}
working_config = production_config
Both variables now refer to the same dictionary. Mutating the dictionary through either variable changes the same underlying object.
A shallow copy creates a new outer object, but nested objects remain shared. For a dictionary containing lists or other dictionaries, this means changing a nested value can still affect the original.
A deep copy recursively copies the outer object and the nested mutable objects inside it. This is usually the safest choice when you need an independent copy of a nested configuration.
- Assignment: two names refer to the same object.
- Shallow copy: a new outer object, with shared nested objects.
- Deep copy: a separate object graph, including nested mutable objects.
Python provides both operations in the copy module. Use copy() when sharing nested data is intentional or irrelevant. Use deepcopy() when changes to a copied configuration must not affect the source.
Basic Example
This example compares all three approaches using a deployment configuration. The configuration contains nested dictionaries, so it makes the difference between shallow and deep copies visible.
from copy import copy, deepcopy
original_config = {
"environment": "production",
"features": {
"audit": True,
"metrics": True,
},
"timeouts": {
"connect": 5,
"read": 30,
},
}
assigned_config = original_config
shallow_config = copy(original_config)
deep_config = deepcopy(original_config)
assigned_config["features"]["audit"] = False
shallow_config["timeouts"]["connect"] = 10
deep_config["features"]["metrics"] = False
deep_config["environment"] = "staging"
print(original_config is assigned_config)
print(original_config is shallow_config)
print(original_config["features"]["audit"])
print(original_config["timeouts"]["connect"])
print(original_config["features"]["metrics"])
print(original_config["environment"])
print(deep_config["features"]["metrics"])
print(deep_config["environment"])
Expected Output
True
False
False
10
True
production
False
staging
How the Code Works
assigned_config is another name for original_config. The expression original_config is assigned_config returns True, confirming that both variables refer to the same dictionary.
copy(original_config) creates a new top-level dictionary. Therefore, original_config is shallow_config returns False. However, the nested dictionaries are still shared:
print(original_config["features"] is shallow_config["features"])
print(original_config["timeouts"] is shallow_config["timeouts"])
Both expressions would print True. As a result, changing assigned_config["features"]["audit"] changes the original configuration, and changing shallow_config["timeouts"]["connect"] also changes the original.
The deep copy has independent nested dictionaries. Changes to deep_config do not affect original_config. This makes deepcopy() useful when creating a temporary configuration for a test, preview environment, or per-request customization.
Notice that changing a top-level value such as deep_config["environment"] is isolated even with a shallow copy, because the top-level dictionary itself is new. The important distinction appears when the value being changed is a nested mutable object.
Another Example
Configuration classes often store a dictionary of settings. A common mistake is to give every instance the same nested defaults. Deep copying the defaults in the constructor gives each profile its own configuration tree.
from copy import deepcopy
DEFAULT_PROFILE = {
"theme": {
"name": "light",
"font_size": 14,
},
"notifications": {
"email": True,
"channels": ["email"],
},
}
class UserProfile:
def __init__(self, username):
self.username = username
self.settings = deepcopy(DEFAULT_PROFILE)
def enable_sms_notifications(self):
self.settings["notifications"]["channels"].append("sms")
self.settings["notifications"]["email"] = False
maya_profile = UserProfile("maya")
liam_profile = UserProfile("liam")
maya_profile.settings["theme"]["name"] = "dark"
maya_profile.enable_sms_notifications()
print(maya_profile.settings["theme"]["name"])
print(maya_profile.settings["notifications"]["channels"])
print(liam_profile.settings["theme"]["name"])
print(liam_profile.settings["notifications"]["channels"])
print(DEFAULT_PROFILE["notifications"]["channels"])
Expected Output
dark
['email', 'sms']
light
['email']
['email']
Each UserProfile receives a separate copy of the nested defaults. Without deepcopy(), Maya’s changes to the nested theme dictionary or notification list could appear in Liam’s profile and in DEFAULT_PROFILE.
Common Mistakes
- Confusing assignment with copying:
new_config = old_configonly creates another reference. Usecopy()ordeepcopy()when you need a new object. - Assuming a shallow copy is fully independent:
copy()protects only the outer dictionary. Nested lists and dictionaries remain shared. - Using
deepcopy()automatically for everything: Deep copying can use more memory and time, especially for large object graphs. Use it when independent nested state is required, not as a reflex. - Checking equality instead of identity:
==checks whether two objects contain equivalent values.ischecks whether two variables refer to the exact same object. - Ignoring special objects: Some objects, such as open files, sockets, locks, or objects connected to external resources, may not copy cleanly. Configuration data made from dictionaries, lists, strings, numbers, booleans, and similar values is generally a good fit.
Try It Yourself
Run the following starter code and create a safe, independent configuration for a staging environment. Change the staging region and add a feature flag without changing production_config.
from copy import deepcopy
production_config = {
"region": "us-east-1",
"feature_flags": {
"new_dashboard": False,
},
"allowed_origins": [
"https://app.example.com",
],
}
staging_config = deepcopy(production_config)
# Add your changes here.
print(production_config)
print(staging_config)
Challenge
Write a function named prepare_test_config that accepts a base configuration and returns an independent test configuration.
- Do not modify the base configuration.
- Change
environmentto"test". - Set the nested
retriesvalue to0. - Append
"https://test.example.com"to the nestedallowed_originslist. - Print both configurations to verify that only the returned configuration changed.
Solution
from copy import deepcopy
def prepare_test_config(base_config):
test_config = deepcopy(base_config)
test_config["environment"] = "test"
test_config["request"]["retries"] = 0
test_config["allowed_origins"].append("https://test.example.com")
return test_config
production_config = {
"environment": "production",
"request": {
"timeout": 30,
"retries": 3,
},
"allowed_origins": [
"https://app.example.com",
],
}
test_config = prepare_test_config(production_config)
print(production_config)
print(test_config)
{'environment': 'production', 'request': {'timeout': 30, 'retries': 3}, 'allowed_origins': ['https://app.example.com']}
{'environment': 'test', 'request': {'timeout': 30, 'retries': 0}, 'allowed_origins': ['https://app.example.com', 'https://test.example.com']}
The function deep-copies the input before changing any nested values. The original production configuration therefore retains its environment, retry count, and allowed-origin list.
Key Takeaways
- Assignment creates another reference to the same object; it does not copy the object.
- A shallow copy creates a new outer container but shares nested mutable objects.
deepcopy()recursively copies nested dictionaries, lists, and other copyable objects.- Use deep copying when creating independent configuration objects or per-instance defaults.
- Choose copying deliberately because deep copies can cost more memory and may not support every object type.



