A list or dict is a container object
In plain words
Putting 99 in a list does not swallow the number into the list.
The list keeps a pointer that says “slot 0 is over there.” Another name can
point at the same list — then both of you are looking at the same tray.
A container does not absorb its contents. It stores references to other objects. That Python-level model is enough to predict everyday code. You do not need CPython's internal storage layout.
Name
A name such as nums lives in a namespace or frame. It is a label, not the list itself.
Container object
The list or dict object lives on the heap. It has identity, mutability, contents, and a refcount.
Element references
List slots and dict values point to other objects. For nested containers, those objects may be lists or dicts too.
Shallow copy copies one container level
In plain words A cheap copy is like getting a new folder and putting the same papers in it. The folder is yours. Scribbling on a paper still shows up in the other folder. A deep copy photocopies the papers too.
A shallow copy creates a new outer container filled with the same inner references. The outer list or dict is independent. Nested objects are shared.
One outer list
original = [[1], [2]] creates an outer list. Its two slots point to two inner list objects.
Two outer lists, shared inner lists
shallow = original.copy() creates a second outer list, but both outer lists point to the same
inner lists.
| Operation | New outer container? | Nested objects shared? | Common use |
|---|---|---|---|
b = a |
No | Yes, because everything is the same object | Create another name for the same container |
b = a.copy() |
Yes | Yes | Copy a flat list or dict |
b = a[:] |
Yes | Yes | The classic slice copy — same result as list(a) |
b = list(a) |
Yes | Yes | Make a shallow list copy from any iterable |
b = copy.deepcopy(a) |
Yes | No — mutable nested objects are cloned | Independent nested data, used deliberately |
The multiplication trap
In plain words
[[0] * 3] * 3 does not make three rows. It makes one row and then points at it
three times. Write into any cell and the whole column seems to change, because there was only
ever one list to write into.
List multiplication repeats references, exactly like a shallow copy. The list comprehension
runs [0] * 3 once per row, so each row is a separate object.
Passing a container passes the object reference
In plain words Hand a function your scorebook and it can write a new score on the page you already have. If it tosses that book aside and starts a blank one, your original book is still on your desk.
The parameter is a local name pointing at the same mutable object as the caller. Mutation is visible. Rebinding the parameter is not.
Caller sees it
scores["math"].append(95) follows references to the original list and changes it in-place.
The caller sees the new score.
Caller does not see it
scores = {"math": []} changes only the local name scores. The caller's
book name still points to the old dict.
The same split shows up in += versus +
These two lines read almost the same and do genuinely different things. On a list,
+= is in-place mutation; a = a + [...] builds a new list and rebinds the name.
a += [2] edits the list
Start with a = [1]; b = a. After a += [2] you get
a = [1, 2] and b = [1, 2], and a is b is still
True. One list, edited where it stands.
a = a + [2] makes a new list
Same start. After a = a + [2] you get a = [1, 2] but
b = [1], and a is b is False. The old list is
untouched; a simply points somewhere else now.
Dict keys and values are references too
In plain words A dict is a labeled tray: each label points at some value. The value can be a list you later append to. The label itself has to stay stable — if the key could change after you filed it, Python could not find it again.
Keys must be hashable, which for built-ins usually means immutable (strings, ints, tuples of immutables). Values can be anything, including mutable lists.
| Question | Plain answer | Why it matters |
|---|---|---|
| Can two names point to one dict? | Yes. alias = profile creates another reference. |
Mutating through either name changes one shared dict. |
| Can a dict value be a list? | Yes. Values can be mutable objects. | profile["skills"].append(...) mutates the nested list. |
| Can a list be a dict key? | No. Lists are mutable and unhashable. | If a key could change after insertion, lookup would become unreliable. |
Does dict.copy() copy nested values? |
No. It is shallow. | Nested lists and dicts remain shared unless you copy them deliberately. |
What you should understand by the end
is answers "same object?"
Use identity checks while learning to confirm whether two names are aliases. In normal app code, prefer == unless checking singletons like None.
Object changes, names stay bound
append, item assignment, and dict updates change existing objects. The address stays the same.
Name changes, object may not
x = ... makes a name point somewhere else. Other aliases keep pointing to the original object.
Choose the copy depth intentionally
Use assignment for sharing, shallow copy for a new outer container, and deep copy only when nested independence is truly required.
A key's hash has to hold still
{[1, 2]: "x"} raises TypeError: unhashable type: 'list'. Swap the list for a tuple and {(1, 2): "x"}[(1, 2)] returns "x" — matching on hash and ==, not on is.
* 3 repeats the arrow, not the object
[[0] * 3] * 3 gives one row referenced three times, so grid[0] is grid[1] is True. Build rows with a comprehension when you want three separate lists.
Paste this whole line: import copy; a = [[1]]; b = a.copy(); b[0].append(2); print(a)
— it prints [[1, 2]], because a and b share the inner list.
Now, in that same session, run c = copy.deepcopy(a); c[0].append(3); print(a, c). The
3 lands only in c. Shallow shares the inner list; deep does not.
A tray of arrows, not a box of values. Two nametags on one tray. A cheap copy as a new tray holding the same inner arrows.