Skip to content
VLSI Mentor

Python Part I · Module 8 · Dictionaries

Copying and Building Dictionaries

Chapter 5.8 spent a whole chapter on what happens when two names refer to one list, and called it the first genuinely subtle thing in the course. Everything it established applies to dictionaries without a single change, so this chapter does not re-derive it — it states the transfer and moves to what is actually new. That turns out to be the nested case, which matters far more for dictionaries than it did for lists, because a dictionary whose values are lists or other dictionaries is an ordinary structure rather than an unusual one. The chapter closes with the other ways to build a dictionary, one of which is itself a copy.

Foundation14 min readPythonPart IFundamentalsDictionariesReferences

Part I · Module 8 · Chapter 8.6 · Copying and Building Dictionaries

1. Assignment Does Not Copy

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a = {"test": "smoke"}
b = a
b["status"] = "PASS"
print(a)
print(b)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'test': 'smoke', 'status': 'PASS'}
{'test': 'smoke', 'status': 'PASS'}

Nothing was added to a, and a has two entries.

If that is surprising, Chapter 5.8 is the chapter to re-read — the model is identical and nothing about dictionaries changes it. In one sentence: b = a does not make a second dictionary; it gives the existing one a second name. There is one dictionary and two ways to reach it, so a change made through either is visible through both.

Both directions work, which is the clearest demonstration that neither name is privileged:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
x = {"n": 1}
y = x
x["p"] = 2
y["q"] = 3
print(x)
print(len(x), len(y))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'n': 1, 'p': 2, 'q': 3}
3 3

Two additions through two different names, both landing in one dictionary.

And rebinding still breaks the connection, exactly as Chapter 8.4 described:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
p = {"k": 1}
q = p
p = {"k": 99}
print(p)
print(q)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'k': 99}
{'k': 1}

p = {...} built a new dictionary and pointed p at it. The original was not touched, so q still shows it.

2. Making an Independent Copy

When you want a second dictionary, ask for one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
orig = {"test": "smoke", "status": "PASS"}
cop = orig.copy()
cop["status"] = "FAIL"
print(orig)
print(cop)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'test': 'smoke', 'status': 'PASS'}
{'test': 'smoke', 'status': 'FAIL'}

orig keeps "PASS". Two dictionaries now exist, and changing an entry in one does nothing to the other.

Adding is independent too:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
o2 = {"a": 1}
c2 = o2.copy()
c2["b"] = 2
print(o2, len(o2))
print(c2, len(c2))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'a': 1} 1
{'a': 1, 'b': 2} 2

One entry against two. The top-level mappings are genuinely separate.

A copy starts out equal, and == cannot tell you whether you have one dictionary or two:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
o3 = {"a": 1}
c3 = o3.copy()
print(o3 == c3)
c3["a"] = 9
print(o3 == c3)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True
False

Equal at first, different once one is changed. Chapter 5.8 made this point for lists and it is worth repeating in one line: a == b is True both when there are two equal dictionaries and when there is one dictionary with two names. Only how the second name was produced decides which you have.

3. The Part That Is Only Half a Copy

Now the section this chapter exists for, and the reason it matters more for dictionaries than it did for lists.

.copy() makes a new outer dictionary. It does not go into the values and duplicate those. For values that cannot change — strings, numbers — that is invisible and harmless. For a value that is a list:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
original = {"smoke": ["PASS", "PASS"]}
clone = original.copy()
clone["smoke"].append("FAIL")
print(original["smoke"])
print(clone["smoke"])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['PASS', 'PASS', 'FAIL']
['PASS', 'PASS', 'FAIL']

Both changed. The copy was made, nothing was done to original, and original now reports three results.

Nothing went wrong with the copy. The two dictionaries really are separate — but they both have a "smoke" key referring to the same list, and clone["smoke"] reached that list and appended to it. Chapter 5.8 called this an aliasing situation, and that is exactly what it is: original["smoke"] and clone["smoke"] are two ways of naming one list.

This kind of copy is called a shallow copy: one level deep.

4. What the Copy Does Protect

It would be easy to over-correct and conclude that .copy() is useless for nested data. It is not — the boundary is precise.

Replacing a whole value in the copy is safe:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
o4 = {"smoke": ["PASS"]}
c4 = o4.copy()
c4["smoke"] = ["FAIL"]
print(o4)
print(c4)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'smoke': ['PASS']}
{'smoke': ['FAIL']}

o4 is untouched. That line changed which list the copy's key refers to — an operation on the copy's own mapping, which is independent. It did not reach into the shared list.

So the rule has two halves, and both are true:

Operation on the copyAffects the original?
c["new"] = v — add an entryno
c["existing"] = v — replace a valueno
del c["k"] — remove an entryno
c["k"].append(x) — change a value's contentsyes

The first three change the copy's mapping. The last one changes a value both mappings point at.

Python does have a way to copy all the way down, and it is not in this module. You do not need it yet, and reaching for it by default is its own mistake — most of the time the right answer is to build the nested value fresh rather than to deep-copy an existing structure.

5. Other Ways to Build One

The canonical purpose pairs copying with construction, and the two turn out to be related.

From keyword arguments — convenient when the keys are simple names:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
d1 = dict(test="smoke", seed=1234)
print(d1)
print(type(d1))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'test': 'smoke', 'seed': 1234}
<class 'dict'>

Note what happened to the keys: test= became the string key "test". This form only works for keys that look like Python names — you could not write dict(runtime-s=1) or use a number as a key this way.

From a list of pairs — the shape Chapter 6.7 called the most common in real data:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
d2 = dict([("a", 1), ("b", 2)])
print(d2)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'a': 1, 'b': 2}

Each (key, value) tuple becomes one entry. This is also exactly what .items() produces, which makes the two forms fit together neatly.

With nothing — the empty dictionary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
print(dict(), len(dict()), dict() == {})
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{} 0 True

dict() and {} produce the same value. Write {} — it is shorter and it is the notation Chapter 8.2 established. dict() earns its place when it is converting something.

From another dictionary — which is a third way to copy:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
src = {"a": 1}
d3 = dict(src)
d3["b"] = 2
print(src, d3)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'a': 1} {'a': 1, 'b': 2}

dict(src) built an independent top-level dictionary, exactly like src.copy() — and with exactly the same shallow boundary from Section 3.

It refuses input that is not pairs:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
print(dict([1, 2]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TypeError: cannot convert dictionary update sequence element #0 to a sequence

A plain list of numbers has no key-value structure to read, and the message says which element it gave up on.

6. There Is No Slice Copy

Chapter 5.8 gave three copying forms for a list: a.copy(), a[:] and list(a). Only two of them transfer:

For a listFor a dictionary
a.copy()d.copy()
list(a)dict(d)
a[:]does not exist

A slice needs positions, and Chapter 8.1 established that a dictionary has none — d[0:1] was KeyError: slice(0, 1, None), the slice object used as a key. So d[:] is not a copy idiom here; prefer d.copy(), which says what it means.

7. The None Trap, Once More

Chapter 8.4 showed it for update. It is worth checking you have it, because copy is the exception:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a = {"x": 1}
a = a.copy()
print(a)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'x': 1}

That is correct — copy() returns the new dictionary, so assigning it is exactly right. Compare with update, which mutates and returns None:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
d = {"a": 1}
d = d.update({"b": 2})
print(d)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
None

8. Predict, Run, Observe

Predict every line before reading on. This is the exercise the chapter exists for.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a = {"name": "smoke", "runs": ["PASS"]}
b = a
c = a.copy()
b["status"] = "done"
print(len(a))
print(len(c))
c["runs"].append("FAIL")
print(a["runs"])
c["name"] = "reset"
print(a["name"])
print(a == c)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3
2
['PASS', 'FAIL']
smoke
False

Work through it. b = a gives one dictionary two names, so b["status"] = "done" makes len(a) 3. c = a.copy() was taken before that, so c has 2 entries and did not receive "status".

c["runs"].append("FAIL") reaches into the list that a and c both refer to — so a["runs"] shows the appended value. That is Section 3.

c["name"] = "reset" replaces which value the copy's key refers to, so a["name"] is still "smoke". That is Section 4.

And a == c is False — different entry counts and a different name.

If you predicted that c["runs"].append(...) would leave a alone, you read .copy() as a full duplicate. If you predicted c["name"] = "reset" would change a, you over-corrected.

9. Exercises

Exercise 1 — one or two. For each, say whether there is one dictionary or two afterwards: (a) b = a (b) b = a.copy() (c) b = dict(a) (d) b = a; b = a.copy() (e) b = {}; b.update(a)

Exercise 2 — predict. Given a = {"k": 1} and b = a, say what b holds after each, and which operation it was: (a) a["k"] = 2 (b) a = {"k": 3} (c) a["new"] = 4 (d) del a["k"]

Exercise 3 — the nested case. Predict both prints, verify, and explain the difference in three sentences:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
o = {"items": ["a"]}
c = o.copy()
c["items"].append("b")
print(o["items"])
c["items"] = ["z"]
print(o["items"])

Exercise 4 — what a copy protects. For a copy c of o, say which of these change o and which do not: (a) c["new"] = 1 (b) c["old"] = 1 (c) del c["old"] (d) c["list_value"].append(1) (e) c["list_value"] = [1]

Exercise 5 — build four ways. Produce {"test": "smoke", "seed": 1234} using a literal, using dict() with keyword arguments, and using dict() with a list of pairs. Say in one sentence which form you would use when the keys come from data rather than being typed out.

Exercise 6 — the missing idiom. Explain in two sentences why d[:] does not copy a dictionary, referring to what a slice needs and what Chapter 8.1 showed d[0:1] actually does.

Exercise 7 — assign or not. For each, say whether assigning the result is correct, and what you get if you assign wrongly: (a) d.copy() (b) d.update(other) (c) d.pop("k") (d) d.clear()

Exercise 8 — the safe design. You have a template record and want twenty independent copies, each of which will gain its own list of results. Explain in three sentences why template.copy() is not sufficient on its own, and what you would do instead using only what this module has taught.

10. Summary

  • b = a does not copy a dictionary. One dictionary, two names — and a change through either is visible through both. The model is Chapter 5.8's, unchanged.
  • Rebinding affects only the name rebound.
  • a.copy() makes an independent top-level dictionary. Adding, replacing and removing entries in the copy do not touch the original.
  • == cannot tell one dictionary from two — only how the second name was produced can.
  • A copy is shallow: it separates the mapping, not the values. c["runs"].append(x) changes a list both dictionaries refer to.
  • Replacing a whole value in the copy is safe, because that changes the copy's own mapping rather than reaching into a shared value.
  • For flat dictionaries of strings and numbers the distinction is invisible. For nested data it is not, and nested is normal.
  • dict(test="smoke") builds from keyword arguments, but only for keys that look like Python names.
  • dict([("a", 1)]) builds from pairs — the shape .items() produces.
  • dict(d) is a third copy form, with the same shallow boundary. d[:] does not exist — a dictionary has no positions to slice.
  • copy() returns the new dictionary, so assign it. update() and clear() return None, so do not.

Nested values have appeared twice now as a complication. The next chapter makes them the subject.

Where this fits

Part of the Python curriculum.