Python Copying and Joining Collections

Tell aliasing, shallow copies and deep copies apart, and combine lists and dictionaries without unwanted side effects.

Lesson 25 of 107Intermediate12 min read

In Python, assignment never copies a list or dictionary; it gives the same object a second name. To get an independent object you make a shallow copy, which duplicates the outer container, or a deep copy, which duplicates everything inside it too:

Python
import copy
teams = {"names": ["Otters"]}
alias = teams                    # same object
shallow = teams.copy()           # new dict, same inner list
deep = copy.deepcopy(teams)      # new dict and new list

Choosing the wrong one causes some of the most confusing bugs in Python: data that changes "by itself". This lesson shows exactly what each kind of copy shares, then covers joining lists and merging dictionaries. The examples manage settings for a five-a-side football tournament.

Aliasing

When you write b = a, both names refer to one object. Any change made through either name is visible through the other:

Python
original = ["Otters", "Herons"]
alias = original
alias.append("Kites")
print(original)          # ['Otters', 'Herons', 'Kites']
print(alias is original) # True

Aliasing also happens when you pass a list to a function: the parameter is another name for the caller's list, so teams.append(...) inside the function changes the caller's data. That's useful when intended and a nasty surprise when not.

Rebinding is different from mutating. alias = ["Swifts"] makes alias point to a new list and leaves original untouched. Problems only arise when you change the shared object in place, with methods like append(), sort() or update(), or with item assignment such as alias[0] = "Swifts".

Shallow copies

A shallow copy creates a new outer container whose slots refer to the same inner objects:

Python
groups = [["Otters", "Herons"], ["Kites"]]
copy_1 = groups.copy()        # also: groups[:], list(groups), copy.copy(groups)
copy_1.append(["Swifts"])     # only copy_1 gets a new group
copy_1[0].append("Owls")      # changes the inner list both share
print(groups)                 # [['Otters', 'Herons', 'Owls'], ['Kites']]

Adding or removing top-level items affects only the copy. Changing a nested list or dictionary affects both, because there's only one of it.

For flat data, such as a list of strings or numbers or a dictionary of simple values, a shallow copy is all you need, and it's fast. Dictionaries copy with d.copy() or dict(d), sets with s.copy(). Slicing a list (items[:]) is a shallow copy too.

Deep copies

copy.deepcopy() copies the container and, recursively, every mutable object inside it, so the result shares nothing changeable with the original:

Python
import copy
settings = {"teams": ["Otters"], "rules": {"minutes": 20}}
draft = copy.deepcopy(settings)
draft["teams"].append("Herons")
draft["rules"]["minutes"] = 25
print(settings)   # {'teams': ['Otters'], 'rules': {'minutes': 20}}

deepcopy keeps a memo of objects it has already copied, so a structure that refers to the same object twice, or even to itself, is copied correctly without infinite loops.

Deep copies cost more time and memory, so use them when nested data really must be independent, for example before editing a template, or to keep an "undo" snapshot. Some objects can't be meaningfully copied, such as open files, network connections and locks; those need to be opened again, not cloned. Classes can control how they are copied by defining __copy__ and __deepcopy__.

Merging

Joining lists: + creates a new list; extend() and += modify the left list in place; unpacking builds a new list from several sources.

Python
group_a, group_b = ["Otters"], ["Herons", "Kites"]
all_teams = group_a + group_b          # new list
combined = [*group_a, *group_b, "Owls"]
group_a.extend(group_b)                # group_a is changed

Merging dictionaries: when keys clash, the value from the later dictionary wins.

Python
defaults = {"minutes": 20, "subs": 3}
overrides = {"minutes": 25}
rules = defaults | overrides           # Python 3.9+: {'minutes': 25, 'subs': 3}
rules = {**defaults, **overrides}      # works on 3.5+
defaults.update(overrides)             # modifies defaults in place

"Later wins" is a policy, not always the right one. If two sources disagree about something important, such as conflicting team rosters, detect the clash explicitly: defaults.keys() & overrides.keys() lists the keys present in both, so you can decide what to do with each. All these merges are shallow: nested values are shared, not copied.

Independent practice and a worked check

Task: compare assignment, shallow copy and deep copy using nested tournament data. Create a dictionary with a nested list of fixtures, make all three kinds of copy, append a fixture through the original, and check which copies see it.

Python
import copy
cup = {"fixtures": [("Otters", "Herons")]}
same, outer, full = cup, cup.copy(), copy.deepcopy(cup)

cup["fixtures"].append(("Kites", "Owls"))

print(len(same["fixtures"]), len(outer["fixtures"]), len(full["fixtures"]))  # 2 2 1
print(outer["fixtures"] is cup["fixtures"])   # True
print(full["fixtures"] is cup["fixtures"])    # False

The is checks confirm which inner list is shared. For the worked check, what stays shared after a shallow copy is the nested objects: the new outer container still points to the same inner lists and dictionaries.

Reference and next practice

The copy module documentation explains shallow and deep copying, and the dictionary union operator is described with the other mapping operations. Continue with collection methods and naming and multiple assignment. Practise with the Python exercises and Python quiz.

Copying and Joining Collections Summary

  • Aliasing: Assignment gives another name to the same collection.
  • Shallow copies: A shallow copy creates a new outer container while sharing nested objects.
  • Deep copies: deepcopy recursively copies supported structures and tracks previously copied objects.
  • Merging: List concatenation creates a new sequence, extend mutates, and later dictionary keys replace earlier duplicates.

Copying and Joining Collections Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Copying and Joining Collections in a small program

Python
from copy import deepcopy
original = {"teams": ["Otters"]}
settings = deepcopy(original)
settings["teams"].append("Herons")
print(original["teams"])
print(settings["teams"])
Output
['Otters']
['Otters', 'Herons']
Line-by-line explanation
  1. Line 3deepcopy creates a new dictionary and a new inner list.
  2. Line 4Appending to the copy's list doesn't touch the original's list.
  3. Lines 5 to 6The original still has one team; the copy has two.
Example 2

Assignment, shallow copy and deep copy side by side

Python
import copy

base = {"name": "Spring Cup", "teams": ["Otters", "Herons"]}

alias = base
shallow = base.copy()
deep = copy.deepcopy(base)

base["name"] = "Summer Cup"
base["teams"].append("Kites")

for label, obj in [("alias", alias), ("shallow", shallow), ("deep", deep)]:
    print(f"{label:<8} {obj['name']:<11} {obj['teams']}")
Output
alias    Summer Cup  ['Otters', 'Herons', 'Kites']
shallow  Spring Cup  ['Otters', 'Herons', 'Kites']
deep     Spring Cup  ['Otters', 'Herons']
Line-by-line explanation
  1. Line 5The alias is the same dictionary, so it sees both changes.
  2. Line 6The shallow copy has its own "name" slot, so the rename doesn't reach it, but it shares the teams list.
  3. Line 7The deep copy shares nothing, so it keeps the original name and teams.

Common Copying and Joining Collections Mistakes

Treating assignment as a copy

Don't
template = ["Otters", "Herons"]
this_year = template
this_year.remove("Herons")      # template loses Herons too
Do
template = ["Otters", "Herons"]
this_year = template.copy()
this_year.remove("Herons")

Assignment only adds a name. Call .copy() when you plan to change the data.

Using a shallow copy for nested data

Don't
rules = {"timings": {"half": 10}}
draft = rules.copy()
draft["timings"]["half"] = 15   # rules changes as well
Do
import copy
rules = {"timings": {"half": 10}}
draft = copy.deepcopy(rules)
draft["timings"]["half"] = 15

Copying and Joining Collections in Real Projects

Copying and merging in real programs

Configuration systems merge layers of settings, built-in defaults, a config file, environment variables and command-line options, with each later layer overriding the earlier one, exactly like defaults | file_settings | cli_settings. Tests deep-copy fixture data so one test can't corrupt the data another test uses. Editors and games deep-copy state to implement undo.

Many bugs reported as "my list changed by itself" turn out to be aliasing, often through a default argument or a function that modifies its input. A common defensive habit is for functions to copy their input before changing it, or to return new objects rather than mutating the ones they were given.

Copying and Joining Collections FAQ

What is the difference between copy and deepcopy in Python?

copy.copy() (shallow) creates a new outer object but reuses the objects inside it. copy.deepcopy() recursively copies everything mutable inside as well. Use a shallow copy for flat data and a deep copy when nested lists or dictionaries must be independent.

How do I copy a list in Python?

Use new = old.copy(), new = old[:] or new = list(old); all three make a shallow copy. For a list containing other lists, use copy.deepcopy(old). Plain new = old doesn't copy at all.

How do I merge two dictionaries in Python?

On Python 3.9 and later, merged = a | b creates a new dictionary, and a |= b updates a. On older versions use {**a, **b} or a.update(b). In every form, values from b replace values from a for duplicate keys.

Does slicing create a copy of a list?

Yes, a shallow one. items[:] and items[1:3] return new lists, so adding or removing items in the slice doesn't affect the original. Nested objects inside are still shared.

Are strings and tuples copied when I assign them?

No, but it doesn't matter, because they can't be changed. Any "change" creates a new object and rebinds the name, so two names sharing an immutable object can never affect each other. Copying only matters for mutable objects.

Copying and Joining Collections Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What stays shared after a shallow copy?

Multiple choicemedium

Which review concern is valid for copying and joining collections?

Copying and Joining Collections Quiz

2 questions to check what you learned.

  1. What stays shared after a shallow copy?

    • The new outer container
    • Nested objects
    • Python ignores the distinction
  2. Which statement correctly describes deep copies?

    • deepcopy recursively copies supported structures and tracks previously copied objects.
    • Only the example output matters, not the rule
    • Every operation has the same behavior