631
@rubberduck_rae ·

My original Python list changes when I only modified the copy Concepts

I did new = old and then appended to new, and old grew too. Then I read about copy() so I used new = old.copy() and that worked for a flat list, but my actual data is a list of dictionaries and modifying a dictionary inside the copy still changes the original. What is going on and what is the correct way to copy this?

8 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @venv_val · 2mo ago · 3 replies

    Two separate ideas are colliding here. new = old does not copy anything, it makes a second name pointing at the same list object, which is why the first version surprised you. old.copy() makes a genuinely new list, but the elements inside it are still the same objects as before, so a list of dicts gives you a new list holding the original dicts.

    For your case:

    import copy
    new = copy.deepcopy(old)
    

    That recursively copies the nested structures. Downside is it is slow on big data and it chokes on things like open files or database connections. If your dicts are plain data, deepcopy is the right call and you should stop thinking about it.

    704
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @rubberduck_rae · 2mo ago

      Shallow versus deep was the missing vocabulary. deepcopy fixed it immediately.

      141
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @vacuum_analyze · 2mo ago

      Worth adding that id(x) in the interpreter is the quickest way to check this yourself. Print id(old[0]) and id(new[0]) and the answer is right there.

      96
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @eleven_tabs · 2mo ago · 2 replies

    The mental model that made this stop biting me: variables in Python are labels tied to objects, not boxes holding values. a = b sticks a second label on one object. Everything about mutable default arguments, aliasing bugs and this exact problem falls out of that single sentence.

    342
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @flat_fix_finn · 2mo ago

      And the corollary that bit me later: def f(items=[]) creates that list once at definition time, so it accumulates across calls. Use None and build inside the function.

      178
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @threat_modeler_j · 2mo ago

    Alternative that often removes the need to copy at all: build the new structure instead of mutating. Something like new = [dict(d, status='done') for d in old] gives you fresh dicts and leaves the original untouched. Code that avoids in place mutation has far fewer of these bugs in the first place.

    267
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @b2_bruno · 2mo ago · 2 replies

    Just use new = old[:], that is the standard way to copy a list.

    24
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @venv_val · 2mo ago

      That is still a shallow copy, identical behaviour to .copy(), so it does not solve the nested dict problem they described.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report