Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
The Object Model  ›  Lesson

Names, Objects & Identity

Data Model 20 minNames point at objects; assignment and calls bind names without copying, and identity is a question you can ask
You're building a piece ofQuarry — the inspector
This piece — shared_names(): Runs a stage's bindings for real and reports which names end up pointing at one object.
Scenario Quarry's audit stage swears cleaning never changes anything, because the cleaner and the auditor have been holding the same rows all along.
Your task
Write shared_names(steps). Each step is [target, op, source]: "new" binds target to a fresh empty list, "alias" binds it to the same object as source, and "copy" binds it to a shallow copy. Run the steps in a dict that acts as a namespace, then return every group of two or more names bound to one object, each group sorted, groups sorted. Example: [["raw","new",null],["clean","alias","raw"],["backup","copy","raw"]] → [["clean","raw"]].

Names, Objects & Identity

In short: a Python name is a label, not a box — it points at an object somewhere in memory, and = always points a name at an object, never copies one. Two names can point at the very same object (aliasing); mutating through either name is visible through both, but rebinding one name (pointing it somewhere else) never touches the other. A function call works the same way — the parameter is a new name bound to whatever object the caller passed, so a function can mutate the caller's object but can never rebind the caller's name. Use == to compare values and save is for true identity questions (None, sentinels) — never numbers or strings, where it can look right by accident.

Two labels "raw" and "cleaned" both pointing at one list before an append; after cleaned = [...] rebinds cleaned onto a new stripped list while raw still points at the original, unchanged

An audit that always says "nothing changed"

A cleaning stage trims whitespace from every field of a batch. An audit stage runs later and compares the cleaned rows against the raw ones — and always reports that nothing changed:

python

Nobody edited raw on purpose. cleaned = raw never copied anything — it just gave the same list a second label, so a change through one name shows up through the other. The fix is for the cleaner to build a new list instead of mutating the one it was handed:

What does this print?
raw = ["alpha ", " beta"]
cleaned = raw
cleaned = [f.strip() for f in cleaned]   # a comprehension builds a NEW list
print(raw)

Step through the same audit code and watch the same column. It's True only while both names point at one object:

step through it
1raw = ["alpha ", " beta"]
2cleaned = raw
3same = cleaned is raw
4cleaned.append("gamma")
5seen_by_raw = len(raw)
6cleaned = [f.strip() for f in cleaned]
7same = cleaned is raw
8first_raw = raw[0]

Drawn out, the end state looks like this:

Drawing diagram…

Until the append, both names pointed at list #1, so it changed the one list they shared. The comprehension built list #2, and = moved one arrow onto it. That is the whole fix for the audit bug: the cleaner should build new rows instead of editing the rows it was handed.

Passing an argument is binding a name

A function call does exactly what = does: the parameter becomes a new name bound to whatever object the caller passed. Nothing is copied — this is called call by sharing, and it's neither "by reference" nor "by value":

What does this print?
def clean(row):
    row.append("checked")     # mutates the object the caller passed
    row = ["fresh"]            # rebinds row — a LOCAL name only
    row.append("new")

row = ["a"]
clean(row)
print(row)

A function that needs to hand back a changed version should return it. A function that mutates its argument should say so in its name.

The grid that repeats one row three times

* on a list repeats references, not copies. For numbers that's harmless, because a number can't be mutated — for rows of a grid it's a trap:

broken — fix it

Only the top-left cell should become 1. Run it, see every row change, then fix the first line so each row is its own list.

The inner [0] * 3 is safe, because it repeats references to one 0, and 0 can't change. The outer * 3 repeats references to one list, and a list can. A comprehension evaluates [0] * 3 fresh on every loop, so each row really is a separate object.

Shallow copy vs. deep copy

When a stage genuinely needs its own version of the data, it copies — but "copy" has two depths. A shallow copy (list(x), x.copy(), x[:]) makes one new outer container, filled with references to the same inner items. A deep copy (copy.deepcopy(x)) walks the whole structure and copies every mutable container it finds, all the way down:

python

That's not a naive recursive copy — a naive one would copy tags twice (breaking the sharing) and recurse forever on the cycle. deepcopy keeps a memo, a dict from id(original) to the copy already made for it, and checks it before copying anything. Shallow-copy when you're only changing the top level; deep-copy when you're about to change something nested; skip copying entirely when a stage can just return new rows instead of editing the ones it was handed.

Quick practice

What does this print?
a = 256
b = 256
x = "".join(["qu", "arry"])
y = "quarry"
print(a is b, x is y)
python

A plain default=None couldn't tell "no default given" apart from "the default really is None". A freshly built object() can, because no caller could ever pass in that exact object by accident — that's what is is actually for.


🎯 Your turn

The inspector's first piece replays how a stage script binds its names, and reports which ones end up sharing an object. Each step is [target, op, source]:

  • [name, "new", null] binds name to a brand-new empty list
  • [name, "alias", other] binds name to the same object other points at
  • [name, "copy", other] binds name to a shallow copy of that object

Write shared_names(steps): run the steps in a dict that plays the part of a namespace, then return every group of two or more names that end up pointing at one object — names sorted within each group, groups sorted.

  • [["raw", "new", null], ["clean", "alias", "raw"], ["backup", "copy", "raw"]] → [["clean", "raw"]]
  • [["a", "new", null], ["b", "alias", "a"], ["a", "new", null]] → [], because rebinding a moved only a

Hint: a dict keyed by id(obj) collects the names sharing each object — ids are safe here because every object stays alive in your namespace while you're looking at it.

Press ▶ Run, then ✓ Check. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upDon't touch the caller's row

Quarry's tagger adds a tag to a row. The first version did row.append(tag), which edited the caller's row behind its back. Write tag_row(row, tag) returning [tagged, row]: tagged is a NEW list with tag on the end, and row is the caller's list, which must come back unchanged.

object-modelaliasing-and-copying
DrillA grid whose rows are really separate

Write marked_grid(rows, cols, cells). Build a rows × cols grid of zeros, set every [r, c] in cells to 1, and return the grid. The starter builds the grid with *, so a mark in one row shows up in every row.

object-modelaliasing-and-copyinglists
BuildA snapshot that stays a snapshot

Quarry's audit keeps a snapshot of a batch before the cleaner strips every field in place. Write clean_with_snapshot(batch): take a snapshot, strip every field of every row IN PLACE (the cleaner must keep editing batch itself), and return {"before": snapshot, "after": batch}. The starter's snapshot gets cleaned too.

aliasing-and-copyingdeep-copyobject-model
BossWhat the next stage sees

Write downstream_view(mode). Build the batch [["a", " b "], ["c"]]. Hand it to a stage in one of three ways, chosen by mode: "alias" passes the batch itself, "shallow" passes list(batch), and "deep" passes copy.deepcopy(batch). The stage appends a new row ["d"] to what it was given, and strips every field of its first row in place. Return [len(batch), batch[0]], which is what the ORIGINAL batch looks like afterwards.

object-modelaliasing-and-copyingshallow-copydeep-copy
CapstoneA deep copy that survives sharing and cycles

Write your own clone(obj, memo=None) that deep-copies nested lists and dicts, returning every other value as it is. Like copy.deepcopy, it must keep a memo from id(original) to its copy, so that an object shared in two places is copied once, and a structure that contains itself is copied without infinite recursion. clone_check(kind) (given) builds a test structure and reports what your clone did with it. Do not use the copy module.

object-modeldeep-copyidentity
ExplainBy reference or by value?

A teammate says: "Python passes lists by reference and ints by value. That's why a function can change my list but not my number." Explain what really happens when you call a function, and why their rule seems to work even though it's wrong.

shared_names.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
shared_names(steps) → listRun a stage's name bindings and group the names that point at one object.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.