Follow names and container entries to objects, distinguish equality from identity, and use identity evidence without relying on implementation-specific object reuse.
You will learn: Trace bindings and references, distinguish rebinding from mutation and equality from identity, use is None, and repair accidental sharing in a small object graph.
Practice in: Google Colab, JupyterLab, or a local editor
There are two names but one list. The assignment sol_gear = nova_gear evaluates the expression on the right and binds another name to that same object. It does not run the list literal again or copy its contents.
This lesson answers four questions that printed values alone cannot settle:
Which object does each name or container position reach?
Did an operation rebind a name or mutate an existing object?
Do two values compare equal, or are they the same object?
When is identity the correct test rather than value equality?
2. A binding connects a name to an object
Use these terms consistently:
Term
Meaning in this lesson
object
a Python value with a type, state/value, and identity
name
an identifier such as nova_gear in a namespace
binding
the relationship from a name to an object
reference
a way for a name or container entry to reach an object
alias
another reference to an object that is already reachable
The list literal creates a list object. The first assignment binds nova_gear. The second assignment adds the alias sol_gear.
Both names reach one list. Rebinding sol_gear later changes its arrow, not the old list or nova_gear.
flowchart LR
A["nova_gear"] --> C["list: lamp, rope"]
B["sol_gear before rebind"] --> C
D["sol_gear after rebind"] --> E["new list: compass"]
The arrows are a learning representation, not syntax Python stores in your source file. They help predict which path will observe a later mutation.
3. Assignment can rebind one name
Continue from one shared list, then rebind only sol_gear:
The new list literal creates a second list. Assignment changes the binding for sol_gear. It does not reach backward through the old binding and replace what nova_gear means.
Compare the two events:
Event
Binding changes?
Existing list state changes?
sol_gear.append("key")
no
yes
sol_gear = ["compass"]
yes
no
This distinction remains important inside functions, loops, and object attributes. Lesson 4 applies it to parameter names.
4. Equality asks about values; identity asks about the object
Create one alias and one equal independent list:
first = ["map", "key"]alias = firstindependent = ["map", "key"]assert first == aliasassert first is aliasassert first == independentassert first isnot independent
== delegates to the values’ equality behavior. For lists, corresponding elements are compared. is does not compare contents; it asks whether both expressions produce the same object.
Four combinations are useful to reason about:
Relationship
==
is
Example
same object, equal to itself
True
True
first and alias
distinct objects, equal contents
True
False
first and independent
distinct objects, unequal contents
False
False
[1] and [2]
same object but unequal to itself
unusual
True
specialized values such as NaN require separate care
The final row is a reminder that equality is behavior defined by a type; it is not the definition of identity. Ordinary beginner data will mostly use the first three rows.
The exact integers do not matter. Do not treat them as memory addresses promised by the Python language, sort them for meaning, save them as permanent record IDs, or compare them across program runs. After an object no longer exists, an implementation may reuse its old ID for a later object.
For ordinary code, left is right communicates an identity question more directly. id is helpful in an evidence table or diagram when several paths must be compared at once.
6. None is a singleton sentinel
Python has one None object. It represents the intentional absence of a value in many APIs:
leader =Noneif leader isNone: message ="Choose a party leader"else: message =f"Leader: {leader}"assert message =="Choose a party leader"
Use is None or is not None. This is an identity question about a documented singleton, not ordinary content equality.
Do not generalize that style to strings or numbers:
Those are value questions, so == is the meaningful operator.
7. Do not learn equality from string or integer reuse
Python implementations may reuse some immutable objects. A literal experiment can therefore make is appear to compare values:
left ="".join(["moon", "light"])right ="moonlight"assert left == right
Do not add an assertion about left is right. Its result is not the value contract and can vary with implementation, optimization, or construction path. The same warning applies to small integers and some tuples or constants.
The robust rule is simple:
use == when the program cares whether ordinary values are equal;
use is when the program explicitly cares about the same object, especially None or a private sentinel created for that purpose.
The two dictionaries are distinct, but their "badges" entries reach one list. An outer identity check cannot answer every nested-sharing question. Follow the complete access path to the object that will mutate.
This explains why nested diagrams matter. A program can have independent outer records and still leak edits through a shared descendant.
9. Rebinding one reference does not remove the others
del inventory removes that name from the current namespace. It does not destroy the list while backup_name still reaches it. Similarly, replacing one dictionary entry leaves an old object available through any other references.
Python automatically manages object lifetime. The language does not require you to count references manually, and this unit does not depend on CPython’s reference-counting details. The useful program-level question is whether any reachable path still leads to the object.
10. Repair sharing only when it is accidental
This construction gives every record the same badges list:
template_badges = ["starter"]players = []for name in ["Nova", "Sol"]: players.append({"name": name, "badges": template_badges})assert players[0]["badges"] is players[1]["badges"]
If badges belong to each player, construct a list for each iteration:
players = []for name in ["Nova", "Sol"]: players.append({"name": name, "badges": ["starter"]})assert players[0]["badges"] == players[1]["badges"]assert players[0]["badges"] isnot players[1]["badges"]
If the shared list represents one team scoreboard that everyone should observe, the first graph may be correct. Identity is not itself a bug. A mismatch between actual sharing and intended ownership is the bug.
11. Lab: map a party roster
Implement helpers that report relationships and build independent player records:
def relationship(left, right):"""Return equal and identical evidence for two values."""raiseNotImplementedErrordef build_player(name, starting_badges, *, leader=None):"""Return a player with an independent badges list and optional leader."""raiseNotImplementedErrordef add_badge(player, badge):"""Append one badge to player and return None."""raiseNotImplementedError
Before implementing, draw three list objects: the source badges, Nova’s badges, and Sol’s badges. Equal initial contents do not require shared identity.
Hint: construct the owned list at the player boundary
relationship returns the results of == and is. In build_player, create a new outer dictionary and use list(starting_badges) for the list that the new player owns. add_badge intentionally mutates that one nested list.
Show one complete solution after attempting the lab
def relationship(left, right):"""Return equal and identical evidence for two values."""return {"equal": left == right, "identical": left is right}def build_player(name, starting_badges, *, leader=None):"""Return a player with an independent badges list and optional leader."""return {"name": name,"badges": list(starting_badges),"leader": leader, }def add_badge(player, badge):"""Append one badge to player and return None.""" player["badges"].append(badge)
Why do the two badges lists compare equal before mutation?
Which assertion proves they are independently owned?
What changes when add_badge runs: a name, an outer dictionary, or a nested list’s state?
Why is leader is None a different kind of question from leader == "Nova"?
Then deliberately make both players share one badges list, predict the leak, observe it once, and restore the independent construction. The failure is useful evidence only when you can point to the exact shared path.
Key points
TipKey points
Assignment binds a name to an evaluated object; it does not copy that object.
Rebinding redirects a name. Mutation changes an object seen through every reference that still reaches it.
== asks about value equality; is asks about object identity.
Use is None for the singleton sentinel and == for ordinary string, number, and collection values.
Treat id() as opaque temporary diagnostic evidence and never rely on literal interning for program correctness.
Containers hold references, so independent outer objects may still share a nested mutable object.
Sharing is correct when it matches ownership; repair only accidental sharing.