Copy vs Change in Place
Lists and dicts change in place. Copy when you must not share. Never give two agents one transcript list.
Some values can change in place. That means you edit the same object. You do not make a new one.
Lists and dicts can change in place. nums.append(3) edits nums. Strings and numbers cannot. name = name + "!" makes a new string. step += 1 makes a new number and moves the name.
If two names point at one list, a change through either name shows up in both. That is the whole lesson. It is also a privacy bug if those names belong to two users.
If both agents point at one list, an append shows up in both chats. Give each agent its own list.
Two names, one listWhat you will learn
- Change in place vs a new value
id()as a light check for “same object”copy.copyvscopy.deepcopy- Why two agents must not share one transcript list
One list, two names
transcript = [class="tok-s">"hello"]
shared = transcript
shared.append(class="tok-s">"oops")
print(transcript) class="tok-c"># ['hello', 'oops']shared = transcript does not copy. It sticks a second name on the same list.
Rebind is different. transcript = ["hello"] later points the name at a new list. The old list is unchanged if another name still holds it. append is not rebind. = of a new list is rebind.
Functions get the same rule. If you pass a list and the function appends, the caller sees the extra item. If the function assigns a new list to its parameter name, the caller’s name is unchanged. Passing a list is not passing a copy.
| Kind | Examples | In-place change? |
|---|---|---|
| Mutable | list, dict, set | yes: append, d[k]=v, add |
| Immutable | str, int, float, bool, tuple, None | no; you make a new value |
A tuple’s slots cannot be replaced. If a slot holds a list, that inner list can still change. Frozen outer, mutable inner: the same story as a frozen dataclass with a dict field.
Rebind vs mutate
| Code | What happens to the caller’s list |
|---|---|
xs.append(row) | caller sees the new row |
xs[0] = row | caller’s first slot changes |
xs = xs + [row] | new list; caller unchanged |
xs = [] | parameter name rebound; caller unchanged |
def record(log, row):
log.append(row)
memory = []
record(memory, class="tok-s">"step 1")
print(memory) class="tok-c"># ['step 1']That can be what you want (record a step). It is a bug when two users share one list. Name the contract: “this function mutates log” or “this function returns a new list.” Silent sharing is the defect. Visible sharing is a tool.
id() lightly
id(x) is a number for that object in memory. Same id means the same object. Different id means two objects.
You do not need id in production code. It is a teaching light: “are these two names the same list?”
a = [class="tok-s">"hi"]
b = a
c = list(a) class="tok-c"># a new list with the same items
print(id(a) == id(b)) class="tok-c"># True
print(id(a) == id(c)) class="tok-c"># Falselist(a) copies the outer list. Inner lists inside a would still be shared. That is a shallow copy: a new box, same insides. a[:] is the same kind of copy for lists. dict(d) is a shallow copy of a dict.
Do not use id of small ints as a uniqueness scheme. Some small integers are interned. Use id only to compare “same list?” while you learn.
copy.copy vs copy.deepcopy
The copy module is stdlib.
| Call | What you get |
|---|---|
copy.copy(x) | New outer list or dict; inner lists still shared |
copy.deepcopy(x) | New outer object and new copies of what is inside |
list(xs) / dict(d) | Shallow copy, like copy.copy for those types |
import copy
a = {class="tok-s">"msgs": [class="tok-s">"hi"]}
shallow = copy.copy(a)
shallow[class="tok-s">"msgs"].append(class="tok-s">"there")
print(a) class="tok-c"># inner list changed — shared
b = {class="tok-s">"msgs": [class="tok-s">"hi"]}
deep = copy.deepcopy(b)
deep[class="tok-s">"msgs"].append(class="tok-s">"there")
print(b) class="tok-c"># still ['hi']Use a deep copy when the object has lists inside lists (or dicts inside dicts) and you must not share any of them. Traces are dicts of lists of dicts. Isolating a trace for a second agent is a deep copy — or, simpler, start from [] and do not share.
Deep copy is slower and can copy too much. Do not deep-copy on every turn as a reflex. Copy when you branch: snapshot before a risky experiment, or split two agents.
Two agents, one transcript
A transcript is a list of messages. If you write:
memory = []
agent_a = {class="tok-s">"name": class="tok-s">"atlas", class="tok-s">"transcript": memory}
agent_b = {class="tok-s">"name": class="tok-s">"bolt", class="tok-s">"transcript": memory}then Atlas’s append also appears in Bolt’s memory. Users can see each other’s prompts. That is a serious bug.
Fix: give each agent its own list.
agent_a = {class="tok-s">"name": class="tok-s">"atlas", class="tok-s">"transcript": []}
agent_b = {class="tok-s">"name": class="tok-s">"bolt", class="tok-s">"transcript": []}If you must start from old rows, copy: list(old) or copy.deepcopy(old) when rows are dicts. list(old) on a list of dicts still shares the dict rows. Editing agent_b["transcript"][0]["content"] would edit Atlas’s first message too. Nested rows need a deep copy, or you must not mutate old rows.
The same bug appears if __init__ does self.log = default_log and every Agent gets the same argument. Pass None and create [] inside, like the defaults lesson. A list on a class body (class Bad: log = []) is shared by every instance. Classes lesson, same object.
Strings and ints hide the issue because assignment makes a new object. a = 1; b = a; b = b + 1 does not change a. Beginners then believe b = a always copies. It copies the label. For lists, the label points at a changeable object. Draw two arrows to one row of boxes. That picture is the lesson.
Walkthrough: shallow copy of a list of dicts
old = [{class="tok-s">"role": class="tok-s">"user", class="tok-s">"content": class="tok-s">"hi"}]
clone = list(old)
clone[0][class="tok-s">"content"] = class="tok-s">"secret"
print(old[0][class="tok-s">"content"]) class="tok-c"># secret — the dict was sharedlist(old) made a new list. It did not copy the dict inside. A second agent that “cloned” a transcript this way still edits the first agent’s words. For rows that are dicts, copy.deepcopy(old) or a loop that builds new dicts: {"role": row["role"], "content": row["content"]}.
What goes wrong
b = aas a copy.- Shallow copy of a trace of dicts, then mutating a row.
- A list default (previous lesson) — same shared object.
- Putting
log = []on a class body so every instance shares it. - Trusting
idof small ints as a uniqueness scheme. - Popping a transcript row and thinking the side effect (sent email) undid itself.
Run to execute this in your browser. Nothing is sent to a server.
Give agent 2 its own list in the last shared block if you change agent2 = agent1. The prints should then diverge. Same id means same list. The final line should show edited: shallow list copy does not copy inner dicts.
How agents use this
Each agent needs its own transcript list. Sharing one list mixes chats. append changes the list in place, so every name on that list sees the new row. Copy before you isolate. Use copy.deepcopy when messages are dicts nested in lists. id() can prove two names are one object while you learn. In real code, just make a new list per agent.
Retries that “roll back” a step must not assume the world rolled back. Popping the last transcript row undoes the log, not the email you sent. Copy-on-write snapshots of state you own are for search trees and what-if plans. Side effects outside Python are not undone by deepcopy.
When you pass state into run(state), decide if run may mutate it. If tests reuse one state dict, mutate will leak between tests. Either copy at the start of run, or treat state as owned by the caller and document that appends are visible.
Global MEMORY = [] at module top is the same bug at file scale. Every request appends to one diary. Return new state. Or pass a per-run list. Tool registries as constant dicts of functions are fine to share: you are not appending user text to them. Share code. Do not share memory.
Prompt compactors should build a new list of small dicts, not delete keys from the live trace. If they mutate, your debug file loses the HTML you needed. Isolation is a copy (or a new object). It is not a comment that says “do not share.”
Check your understanding