Tuesday, October 13, 2026||
MultiKhabarMultiKhabar
Home/technology/How Shallow and Deep Copies Work in Python

How Shallow and Deep Copies Work in Python

Ha
HackerNoon
By Sohel Alam
10/12/2026, 4:55:52 PM
Share:WhatsAppFacebookX (Twitter)Telegram
How Shallow and Deep Copies Work in Python
EXECUTIVE SUMMARY

I copied a Python dictionary, but the original still changed. Here's how shallow copies, shared references, and deep copies actually work.

Key Takeaways

  • How Shallow and Deep Copies Work in Python.
  • I copied a Python dictionary, but the original still changed.
  • Here's how shallow copies, shared references, and deep copies actually work.

A few weeks ago I was testing a small feature nothing fancy, just duplicating a dictionary before running an update, the way you're taught to "be safe." I only touched the copy. And somehow, the original changed too. I hadn't touched it. I hadn't reassigned it. It just changed.It wasn't a Python bug. It was something I'd been getting wrong without realizing it: a variable doesn't hold an object it holds a reference to one. Once that clicked, this entire category of "silent" bugs stopped feeling random.What You Expectoriginal = {"name": "Sohel", "skills": ["Python", "JS"]} copy_dict = original.copy() copy_dict["skills"].append("React") print(original["skills"]) You'd expect this to print:['Python', 'JS'] You made a copy. You only modified the copy. The original should be untouched, right?What Actually Happens['Python', 'JS', 'React'] The original dictionary changed even though you never touched original directly. You only touched copy_dict.First, There's Not Always a Copy at AllBefore looking at .copy(), consider this:original = {"name": "Sohel"} backup = original backup["name"] = "Alex" print(original) Output:{'name': 'Alex'} Nothing was copied here. backup and original are two names pointing at the same dictionary object. Change it through one name, and you see the change through the other.print(original is backup) # True This is called aliasing, and it's worth keeping in your back pocket because it's the same underlying idea .copy() runs into next, just without the false sense of safety.Why This Happens: Shallow Copy Doesn't Mean What You Thinkdict.copy() creates a new dictionary object, but it does not create new objects for the values inside it. It just copies the references to those values.So when you do:copy_dict = original.copy() Python creates a new dictionary, but the value at "skills" in both original and copy_dict is still pointing to the same list object.You can prove this:print(original["skills"] is copy_dict["skills"]) # True is checks object identity not value equality and it returns True. Both keys are pointing to the exact same list object. Two boxes, one shared item inside.original copy_dict ┌─────────────────┐ ┌─────────────────┐ │ name: "Sohel" │ │ name: "Sohel" │ │ skills: ●────────┼───┐ ┌───┼───────────● skills └─────────────────┘ │ │ └─────────────────┘ ▼ ▼ ["Python", "JS", "React"] (one shared list) .append() doesn't create a new list it mutates the existing one in place. Since both dictionaries are pointing at that same list, both "see" the change. This is the same trap you'd run into with shared lists just wearing a dictionary's clothes.Mutation and Reassignment Are Not the Same Thing.append() mutates the existing list the object stays the same, its contents change. Compare that to reassigning copy_dict["skills"] to a brand-new list instead:original = {"skills": ["Python", "JS"]} copy_dict = original.copy() copy_dict["skills"] = ["Python", "JS", "React"] print(original["skills"]) print(copy_dict["skills"]) Output:['Python', 'JS'] ['Python', 'JS', 'React'] This time original is untouched because copy_dict["skills"] now points to a different list entirely. It never modified the old one. Mutation changes an object that other references can still see.Reassignment points a key to a new object, leaving the old one and anyone else pointing at it alone.Whenever a dictionary seems to "change on its own," it's worth checking which of the two happened: was a shared object mutated in place, or was a key simply reassigned to something new? Only the first one leaks across shared references.It Gets Worse With Nested Dictionariesuser = { "profile": {"theme": "dark", "language": "en"} } backup = user.copy() backup["profile"]["theme"] = "light" print(user["profile"]["theme"]) Output:light Same problem. user.copy() copied the outer dictionary, but "profile" still points to the same nested dictionary object. You can confirm both halves of that:print(user is backup) # False print(user["profile"] is backup["profile"]) # True The outer dictionaries are different objects. The nested profile dictionary is not there was only ever one. Change it through backup, and user sees the change too.The Half-Fix Everyone Reaches For (And Why It's Not Enough)A lot of developers "fix" this by writing their own manual copy:copy_dict = {key: value for key, value in original.items()} This looks like a fresh copy. It isn't. It's exactly the same shallow copy .copy() already gives you just written by hand. The values are still shared references. This "fix" changes nothing about the actual bug; it just adds a false sense of security.When You Actually Need copy.deepcopy()If your dictionary contains mutable objects (lists, other dicts, sets) and you want a truly independent copy, you need a deep copy one that recursively copies every nested object, not just the top level.import copy original = {"name": "Sohel", "skills": ["Python", "JS"]} deep_copy = copy.deepcopy(original) deep_copy["skills"].append("React") print(original["skills"]) # ['Python', 'JS'] print(deep_copy["skills"]) # ['Python', 'JS', 'React'] Now original and deep_copy no longer share the skills list modifying one doesn't touch the other.One nuance worth knowing: deepcopy() keeps track of objects it has already copied. So if two keys in your original dictionary pointed to the same nested object, the copy preserves that relationship between the copies too it isn't shared with the original anymore, but it's still shared within the copy. It's not simply "everything becomes independent of everything"; it's "the copy no longer shares state with the original."Don't Deep-Copy EverythingOnce you understand the problem, it's tempting to reach for deepcopy() by default, everywhere, "just to be safe."That's usually overkill. Deep copying walks the entire nested structure and rebuilds it which costs more memory and more time than a shallow copy, especially on large or deeply nested data. If your dictionary doesn't contain anything mutable, or you're only replacing top-level values, .copy() already does the job:config = {"version": 2, "debug": False} backup = config.copy() backup["version"] = 3 print(config) Output:{'version': 2, 'debug': False} There's no shared mutable object being changed here, so a shallow copy is enough. Reach for deepcopy() when you actually need it not as a default habit.The Other Silent Trap: Mutable Default ArgumentsIf you've read my earlier piece on functions secretly changing data you never passed to them, this next part will look familiar because it's the exact same root cause, just showing up through a function signature instead of .copy():def add_item(item, cart={}): cart[item] = cart.get(item, 0) + 1 return cart print(add_item("laptop")) print(add_item("mouse")) Expected output:{'laptop': 1} {'mouse': 1} Actual output:{'laptop': 1} {'laptop': 1, 'mouse': 1} The default argument cart={} is created once, when the function is defined — not each time it's called. Every call without an explicit cart argument reuses that same dictionary object. It's not a copying bug this time, but the root cause is identical to everything above: unintended sharing of a mutable object across places that expected to be independent — the same trap, wearing a function's clothes this time instead of a dictionary's.Fix:def add_item(item, cart=None): if cart is None: cart = {} cart[item] = cart.get(item, 0) + 1 return cart Now every call that doesn't pass cart gets a fresh dictionary.Quick Reference: What's Safe to Shallow-CopyNot every case needs deepcopy(). Whether .copy() is enough depends entirely on what's inside the dictionary:Value type in dictIs a shallow copy safe?int, float, str, boolYes — these can't be mutated in place at alltuple containing only immutable valuesYes — same reasonlist, dict, setOnly if you don't need it independent from the originaltuple containing a mutable value (e.g. ([1, 2],))Only if you don't need that inner list independent — it's still sharedWhich Copy Should You Actually Use?Honestly, the easiest way to decide is to just ask yourself what kind of independence you need.If your dictionary only holds immutable values, or you're genuinely fine with nested objects staying shared, or you only ever plan to reassign keys rather than mutate what's inside them a plain .copy() is enough, and reaching for anything heavier is wasted effort.If it holds nested lists, dicts, or sets, and you need the copy to be completely unaffected by changes to the original (or vice versa), that's when copy.deepcopy() earns its keep.And sometimes you don't want a copy at all you want two names pointing at the same dictionary, because that's exactly the behavior you're relying on somewhere else in your code. That's not a bug either. That's just aliasing, used on purpose.The Mental Model to RememberA Python dictionary's .copy() only copies one level deep. Anything mutable sitting inside it a list, another dict, a set isn't duplicated. It's shared. Two dictionaries, one object living inside both.The surprising part isn't that .copy() failed it didn't. It performed a shallow copy exactly as designed. The surprise only happens when you expect "copy" to mean "fully independent" without checking what's nested inside. And that doesn't automatically mean the fix is deepcopy() sometimes the fix is just noticing you were mutating something you meant to reassign, or that the sharing was fine all along.The whole picture comes down to one chain:Aliasing (no copy same object under two names) → shallow copy (new outer dictionary, same nested objects) → mutation vs. reassignment (which one you do determines what leaks) → deep copy (independent all the way down, when you actually need it).Once I started placing a piece of code correctly on that chain, "the dictionary changed and I never touched it" stopped being a mystery it became just another Tuesday debugging session.

Share:WhatsAppFacebookX (Twitter)Telegram

Read Complete Original Article

Published originally on HackerNoon

Visit Publisher Site →

Related News

‘डिजिटल त्रिवि’देखि ‘रिसर्च युनिभर्सिटी’सम्मः के–के हुँदैछन् नयाँ काम?
प्रविधिJust now

‘डिजिटल त्रिवि’देखि ‘रिसर्च युनिभर्सिटी’सम्मः के–के हुँदैछन् नयाँ काम?

त्रिभुवन विश्वविद्यालयले आफ्नो प्रशासनिक, शैक्षिक तथा प्राज्ञिक संरचनालाई समयानुकूल बनाउँदै डिजिटल सेवा विस्तार, अनुसन्धान प्रवर्द्धन, संस्थागत पुनःसंरचना र वित्तीय सुधारका नयाँ योजना अघि सारेको छ। त्रिविका सहकुलपति एवं शिक्षा तथा खेलकुदमन्त्री सस्मित पोखरेलको अध्यक्षतामा बसेको विश्वविद्यालय सभामा चालु आर्थिक वर्षको नीति तथा कार्यक्रम प्रस्तुत गरिएको थियो। यी योजना कार्यान्वयन भएमा विश्वविद्यालयको प्रशासनिक, शैक्षिक र प्राज्ञिक स्वरूपमा ठूलो फेरबदल आउने अपेक्षा गरिएको ...

टेक्नोलोजी खबर
इन्टरनेट प्याकेज र शुल्कमा प्राधिकरणको निगरानी, १४ सेवा प्रदायक कारबाहीमा
प्रविधिJust now

इन्टरनेट प्याकेज र शुल्कमा प्राधिकरणको निगरानी, १४ सेवा प्रदायक कारबाहीमा

काठमाडौं । नेपाल दूरसञ्चार प्राधिकरणले स्वीकृत मापदण्ड र निर्देशनविपरीत इन्टरनेट सेवा सञ्चालन गरेको भन्दै १४ वटा इन्टरनेट सेवा प्रदायक कम्पनीलाई जनही ५० हजार रुपैयाँ जरिवाना गरेको छ ।प्राधिकरणले मंगलबार जारी गरेको प्रेस विज्ञप्तिअनुसार अनुमतिपत्रका सर्त उल्लङ्घन गरेको निरीक्षण तथा जाँचबुझबाट पुष्टि भएपछि दूरसञ्चार ऐन, २०५३ को दफा ४७ (१) बमोजिम कारबाही गरिएको हो ।प्राधिकरणका प्रवक्ता मिनप्रसाद अर्यालका अनुसार स्वीकृत प्याकेजअनुसार ...

आइसिटी समाचार
नेपालमा सार्वजनिक भयो भेस्पा १५० एफआई ओबीडी २ स्टेज बी: विशेषता तथा विवरण
प्रविधिJust now

नेपालमा सार्वजनिक भयो भेस्पा १५० एफआई ओबीडी २ स्टेज बी: विशेषता तथा विवरण

२७ असोज, काठमाडौं। इटालियन स्कुटर ब्रान्ड भेस्पाले नेपाली बजारमा नयाँ १५० सिसी क्षमताको स्कुटर सार्वजनिक गरेको छ । नेपालका लागि भेस्पाको आधिकारिक वितरक डेल्टा अटोकर्प प्रालिले भेस्पा १५० एफआई ओबीडी २ स्टेज बी (Vespa 150 FI OBD 2 Stage B) सार्वजनिक गरेको हो । काठमाडौँको त्रिपुरेश्वरस्थित डेल्टा अटोकर्पको शोरुममा आयोजित विशेष कार्यक्रममार्फत नयाँ मोडल सार्वजनिक गरिएको […] The post नेपालमा सार्वजनिक ...

आइसिटी फ्रेम
आफूखुशी शुल्क बढाउने १४ इन्टरनेट सेवा प्रदायकलाई प्राधिकरणले तिरायो जरिवाना, स्वीकृति नलिई लागू गरिएका प्याकेज र शुल्क तत्काल बन्द गर्न निर्देशन
प्रविधिJust now

आफूखुशी शुल्क बढाउने १४ इन्टरनेट सेवा प्रदायकलाई प्राधिकरणले तिरायो जरिवाना, स्वीकृति नलिई लागू गरिएका प्याकेज र शुल्क तत्काल बन्द गर्न निर्देशन

नेपाल दूरसञ्चार प्राधिकरणले अनुमतिपत्रका शर्तविपरीत काम गरेको भन्दै १४ वटा इन्टरनेट सेवा प्रदायकलाई जनही ५० हजार रुपैयाँ जरिवाना गरेको छ। प्राधिकरणले आज एक विज्ञप्ति जारी गर्दै इन्टरनेट (इमेलसहित) सेवा सञ्चालन गर्न अनुमति पाएका सेवा प्रदायकले अनुमतिपत्रका सर्त तथा प्रचलित कानुनअनुसार सेवा सञ्चालन नगरेको निरीक्षण तथा जाँचबुझ प्रतिवेदनका आधारमा जरिवाना गर्ने निर्णय गरिएको जनाएको छ। दूरसञ्चार ऐन, २०५३ को दफा ४७(१) बमोजिम ती ...

टेक्नोलोजी खबर
Multikhabar App

All Information In One Place

Real-time headlines from trusted sources. Get the app now — stay updated, inspired, and ahead with breaking news!

Get it on Google Play