Skip to content
beginner

Copying and Referencing in Python: Avoiding Common Pitfalls

You copy a list in Python, change the "copy," and the original changes too. If you have hit this, you are not alone. It is one of the most common beginner…

Published 2026-09-05Updated 2026-09-128 min read
Vibrant close-up image of a pink cosmos flower with yellow center, perfect for floral themes.
Vibrant close-up image of a pink cosmos flower with yellow center, perfect for floral themes. Photo by Siegfried Poepperl on Pexels.

You copy a list in Python, change the "copy," and the original changes too. If you have hit this, you are not alone. It is one of the most common beginner surprises in the language, and it happens because of a hidden assumption about how variables work.

Most beginners picture a variable as a box that holds a value. Assign b = a, and you imagine Python duplicating the contents of box a into box b. That mental model works for numbers and strings, but it falls apart the moment you use a list or dictionary.

Here is the stronger model: a Python variable is a name tag attached to an object. When you write b = a, you are not copying anything. You are sticking a second name tag on the same object. Both names now point at one thing, and any change you make through either name is visible through the other.

This is what programmers call a reference. In plain English: b does not hold a copy of a's data. It points to the same underlying object that a points to.

Why Your "Copy" Changed the Original

Let us make the problem concrete. Run this small script:

original = [1, 2, 3]
copy = original
copy.append(4)

print("original:", original)
print("copy:    ", copy)

Expected output:

original: [1, 2, 3, 4]
copy:     [1, 2, 3, 4]

You changed copy, but original changed too. That feels like a bug, but Python is doing exactly what you asked. The line copy = original did not create a new list. It attached the name copy to the same list object that original already pointed to. One list, two name tags.

Common mistake: Treating = as a copy command. In Python, = only creates a new reference to the same object. If you want an actual copy, you have to ask for one explicitly.

Knowledge check

Check your understanding

Answer this question before you continue.

What does `copy = original` do when `original` is a list?
Misconception Check

Focus: Distinguish assigning a second reference from creating an independent list copy.

Mutable vs Immutable: Why Some Types Behave Differently

You might wonder why this never seemed to matter with numbers or strings. Try this:

a = 5
b = a
b = 10

print("a:", a)
print("b:", b)

Expected output:

a: 5
b: 10

Here, a stays 5. Why the difference?

The answer comes down to whether the object can change in place. Python types split into two camps:

  • Mutable types can be modified after creation. Lists, dictionaries, and sets are mutable.
  • Immutable types cannot be modified after creation. Integers, strings, and tuples are immutable.

When you wrote b = 10, Python did not modify the object that a points to. It created a brand new integer object and attached the name b to it. The original object holding 5 was never touched, so a kept pointing at it.

Lists do not work that way. When you call copy.append(4), Python modifies the list object in place. Since both original and copy point to that same list object, the change is visible through both names.

The rule to remember: reassigning a name creates a new binding, but modifying a mutable object changes the object itself. If two names point at the same mutable object, both names see every in-place change.

Knowledge check

Check your understanding

Answer this question before you continue.

Suppose `a` and `b` refer to the same list. What happens when `b.append(4)` runs?
Single Choice

Focus: Explain why modifying a shared mutable object affects every name that references it.

Making a Real Copy with .copy()

When you actually want a separate list or dictionary, Python gives you a method for it. Both lists and dictionaries have a .copy() method that creates a new outer container:

original = [1, 2, 3]
copy = original.copy()
copy.append(4)

print("original:", original)
print("copy:    ", copy)

Expected output:

original: [1, 2, 3]
copy:     [1, 2, 3, 4]

Now the two lists are independent. You can modify copy without touching original.

This works the same way for dictionaries:

settings = {"theme": "dark", "volume": 70}
backup = settings.copy()
backup["volume"] = 30

print("settings:", settings)
print("backup:  ", backup)

Expected output:

settings: {'theme': 'dark', 'volume': 70}
backup:   {'theme': 'dark', 'volume': 30}

What .copy() gives you is called a shallow copy. That term matters, because shallow copies have a limit you need to understand.

When Shallow Copy Is Not Enough

Three side-by-side panels compare assignment, shallow copy, and deep copy for a nested list. Assignment shares the outer and inner lists, shallow copy separates the outer list but shares the inner lists, and deep copy separates both outer and inner lists; shared objects are marked with matching connection lines.
The level of copying determines which nested objects remain shared and where a mutation can leak back to the original.

A shallow copy creates a new outer container, but the items inside that container are still references to the same objects as the original. For a flat list of numbers, that is fine, because numbers are immutable. But what happens when the list contains other lists?

original = [[1, 2], [3, 4]]
shallow = original.copy()

shallow[0][0] = "X"

print("original:", original)
print("shallow: ", shallow)

Expected output:

original: [['X', 2], [3, 4]]
shallow:  [['X', 2], [3, 4]]

The outer list was copied, but the inner lists were not. Both original and shallow share the same two inner list objects. Change an inner list through one name, and the change shows up through the other.

This is the classic shallow copy trap. It bites whenever your data is nested: a list of lists, a list of dictionaries, or a dictionary whose values are themselves lists or dictionaries.

To copy the inner objects too, you need a deep copy. Python provides one in the copy module:

import copy

original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)

deep[0][0] = "X"

print("original:", original)
print("deep:    ", deep)

Expected output:

original: [[1, 2], [3, 4]]
deep:    [['X', 2], [3, 4]]

copy.deepcopy() recursively copies the outer container and every object inside it, down through however many levels of nesting exist. In this example, the result is fully independent: each inner list is a new object, so changing one through deep leaves original untouched.

Knowledge check

Check your understanding

Answer this question before you continue.

What is printed by this code?
Output Prediction

Focus: Predict how a shallow copy behaves when a nested mutable item is changed.

original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0][0] = "X"
print(original)

Choosing the Right Copy for the Job

You now have three ways to create a second name or a second object. The table below shows when each one is the right tool.

ApproachWhat it doesUse this whenAvoid when
b = aAdds a new name tag to the same objectYou actually want two names pointing at one object, such as passing data to a functionYou need an independent copy
b = a.copy()Creates a new outer container, but inner items are still sharedYou need a new outer container and will not modify nested mutable items independentlyYou need to change nested lists or dictionaries without affecting the original
copy.deepcopy(a)Recursively copies the container and the objects inside itYou need to modify nested mutable items independently and the objects support deep copyingYour data is flat and .copy() is enough, or you want to keep sharing some inner objects on purpose

My rule of thumb for beginners: if your data is flat, use .copy(). If your data is nested and you need to modify the inner objects independently, use copy.deepcopy(). Nesting alone is not the real test. The real question is whether you need changes at the inner level to stay isolated. If you only need a new outer container, a shallow copy is enough.

And if you are not sure whether you need a copy at all, ask yourself whether you are okay with changes appearing in both places. If you are not, copy first.

Knowledge check

Check your understanding

Answer this question before you continue.

You need to edit nested lists in a copied data structure without changing the original. Which approach should you use?
Single Choice

Focus: Choose deep copying when nested mutable data must be modified independently.

Where This Bites in Real Code

This is not abstract theory. Real scripts hit this constantly.

Imagine you load a list of student records and want to build a cleaned-up version for a report. If you write cleaned = records and then start removing or modifying entries, you are mutating your original data. Later, when you need the raw records again, they are gone.

The same pattern appears with configuration dictionaries. You might load default settings and want to experiment with different values for one run. If you copy with =, your experiment overwrites the defaults. A shallow copy protects the outer dictionary. But if you need to change a nested value, like a list of allowed servers or a dictionary of feature flags, ask yourself one question first: should that change affect the original defaults?

If the answer is no, use copy.deepcopy() before editing. If sharing the nested values is fine, a shallow copy is enough. The copy depth follows from which level you plan to mutate, not from the shape of the data alone.

Data cleanup, report building, and file workflows all share this shape: load data, transform it, and keep the original intact for later steps. Choosing the right copy operation is what keeps those steps from interfering with each other.

Your Next Step

Run the nested-list example yourself. Try all three approaches: plain =, .copy(), and copy.deepcopy(). Modify an inner item with each one and observe which changes leak through to the original.

That small experiment will cement the mental model faster than any explanation. Once you can predict which copy method isolates your changes, you have mastered one of the most common sources of confusing Python bugs.

From here, the natural next step is putting these data structures to work: reading data from files, transforming it, and building something useful with it.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

You want to experiment with a configuration containing a nested list, while preserving the defaults. Which replacement fixes the bug in `experiment = defaults`?
Question 1 of 2Debugging

Focus: Select a copying strategy that keeps nested configuration defaults unchanged during experimentation.

defaults = {"allowed_servers": ["a.example"]}
experiment = defaults
experiment["allowed_servers"].append("b.example")
After this code runs, why does `a` still refer to `5`?
Question 2 of 2Misconception Check

Focus: Distinguish rebinding an immutable value from mutating a shared mutable object.

a = 5
b = a
b = 10

References

  1. copy — Shallow and deep copy operations — Python 3.14.7 documentationdocs.python.org
6sources checked
6source domains
5searches run

Research updated Sep 5, 2026

Practical resource

Want a more structured Python path?

Use the Python Starter Pack to turn scattered tutorials into a focused practice path.

View the bundle
Coming soon

Python Starter Pack

A compact LearnPyFast PDF pack covering what Python is, installation, your first program, running Python code, and Python versions.

$9
PDF BundleTopic PackPythonBeginner
  • 5 curated chapters
  • Enhanced PDF edition with bundle-only learning guidance
  • Offline-friendly format for focused review
  • Source article links for future online updates

Coming soon

Free Python bundle

Get the LearnPyFast Python Starter Bundle

A focused collection of beginner-friendly Python resources to help you move from setup to building practical projects.

You’ll receive the bundle by email. You can unsubscribe anytime.

No spam. You can unsubscribe anytime. See our Privacy policy.

Related sites

Continue beyond Python

Explore related Worldmonger sites when you want to move from Python basics into JavaScript or LLM application building.

JavaScript tutorialstutorial

LearnJSFast

Beginner-friendly JavaScript tutorials for practical web development and self-taught developers.

JavaScriptFrontendWeb development
Visit LearnJSFast
LLM tutorialstutorial

LearnLLMFast

Practical LLM tutorials for builders who want to understand prompting, workflows, agents, and AI applications.

LLMAIBuilders
Visit LearnLLMFast

Keep learning

Related tutorials

Continue with nearby Python topics and beginner-friendly explanations.

Vibrant autumn landscape featuring a solitary oak tree in a green field under a cloudy sky.
beginner
10 min read

Basic File I/O in Python

A Python program that never touches a file forgets everything the moment it exits. File I/O is how your code keeps data after the run ends—saving notes,…

Read tutorial
Teacher conducting a lesson with engaged students in a modern classroom setting.
beginner
11 min read

How to Count Words in Python

Counting words in Python sounds trivial until you try it on real text. The moment your sentence contains a comma, a capital letter, or an ellipsis, the…

Read tutorial
Aerial view of a sunny beach in Corpus Christi with waves and coastal buildings.
beginner
9 min read

Dictionaries in Python

Dictionaries have been my favorite data structure throughout my 20 years as a software engineer. When I interviewed at Google, one of the problems I was…

Read tutorial