PYTHON / VARIABLES AND DATA TYPES
Objects, identity, and references
Distinguish a name from the object it points at, use is/id() to detect aliasing, and predict when two names share the same state.
What you will learn
- Explain why assignment binds a name to an object instead of copying the object
- Choose between `is` (same object) and `==` (equal value) correctly
- Detect aliasing with `is` or `id()` and break it with an explicit copy
- Predict whether a function that assigns to its parameter affects the caller
Understanding Objects, identity, and references
Every value in Python is an object, and every object carries three things: a type, a value, and an identity that stays fixed for as long as the object lives. A name like `total` is not the object and does not contain the object; it is a label bound to it. That is why assignment never copies: `b = a` adds a second label to the object `a` already refers to, and both labels are equally direct references to it.
Because a name is only a reference, Python gives you two different questions to ask about two names. `a == b` asks whether the objects compare as equal, which runs the type's own comparison logic. `a is b` asks whether the two names refer to one single object, which is a check of identity and is exactly the same as `id(a) == id(b)`. Two lists with the same contents are equal but not identical; one list with two names is both.
When two names refer to the same mutable object they are aliases, and a change made through one name is immediately visible through the other, because there is only one object to change. The same mechanism governs function calls: the argument object is bound to the parameter name, so mutating it inside the function affects the caller, while assigning a new object to the parameter name only rebinds that local label. If you want independence, you must create a new object yourself, for example with `list(x)`, `dict(x)`, or `copy.deepcopy(x)`.
a = [1, 2, 3]
b = a # second name for the same object
c = [1, 2, 3] # a new object that happens to be equal
print(a == b, a is b)
print(a == c, a is c)
print(id(a) == id(b), id(a) == id(c))
b.append(4) # mutates the one object a and b share
print(a)
print(c)A name is a reference to an object, so assignment rebinds labels rather than copying data, and identity (`is`) is a different question from equality (`==`).
Worked examples
is vs == on numbers and None
Shows why `is` is unsafe for comparing values but is the right tool for None.
big1 = int("1000")
big2 = int("1000")
print(big1 == big2, big1 is big2)
small1 = int("100")
small2 = int("100")
print(small1 == small2, small1 is small2)
value = None
print(value is None)Example explained
Line 1`int("1000")` builds a fresh int object each time, so the two names are equal but not identical.
Line 2CPython pre-creates the ints from -5 to 256, so `int("100")` hands back the same cached object twice and `is` says True.
Line 3The same expression giving False for 1000 and True for 100 is exactly why `is` must never be used to test numeric equality.
Line 4`None` is a singleton: only one None object ever exists, so `value is None` is the idiomatic and reliable check.
What a function can and cannot change
Demonstrates that mutating the argument object reaches the caller, while rebinding the parameter name does not.
def add_item(bag, item):
bag.append(item)
print("same object:", bag is basket)
def replace(bag):
bag = ["new"]
print("same object:", bag is basket)
basket = ["apple"]
add_item(basket, "pear")
print(basket)
replace(basket)
print(basket)Example explained
Line 1Calling `add_item(basket, ...)` binds the parameter `bag` to the very object `basket` names, so `bag is basket` is True.
Line 2`bag.append(item)` changes that shared object, and the caller sees the change through its own name.
Line 3In `replace`, `bag = ["new"]` rebinds only the local name to a brand new list, so `bag is basket` becomes False.
Line 4The caller's list is untouched afterwards, because nothing was ever done to the object itself.
Shallow copy still shares the inner objects
Shows that copying a container copies the references it holds, not the objects those references point to.
import copy
original = [[1, 2], [3, 4]]
shallow = list(original)
deep = copy.deepcopy(original)
print(shallow is original, shallow[0] is original[0])
print(deep is original, deep[0] is original[0])
original[0].append(99)
print(shallow[0])
print(deep[0])Example explained
Line 1`list(original)` builds a new outer list, so `shallow is original` is False.
Line 2That new list holds the same two references, so `shallow[0] is original[0]` is True: the inner lists are shared.
Line 3`copy.deepcopy` rebuilds the inner objects too, so none of its elements are identical to the originals.
Line 4Appending to `original[0]` therefore shows up in `shallow[0]` but not in `deep[0]`.
Tracking identity across rebinding
Uses id() to show that rebinding a name leaves the old object alone.
text = "start"
first_id = id(text)
text = text + "ed"
print(text)
print(id(text) == first_id)
nums = [1]
nums_id = id(nums)
nums.append(2)
print(nums, id(nums) == nums_id)Example explained
Line 1`text + "ed"` cannot alter the existing string, so it produces a new object and the name is rebound to it.
Line 2The stored id no longer matches, which proves `text` now references a different object.
Line 3`nums.append(2)` changes the list in place, so the object's identity is unchanged and the id still matches.
Line 4Same syntax shape, two outcomes: whether identity survives depends on rebinding versus mutating.
Important notes
`id()` values are only unique among objects alive at the same moment; once an object is freed its id can be reused by a new object, so never store ids as long-lived keys.
Small integer caching and string interning are CPython optimizations, not language rules, so code whose correctness depends on `is` returning True for equal values can break on another interpreter or version.
Common mistakes
Writing `if count is 1000:` to test a value; it may accidentally work for small integers but fails once the number is computed at runtime or exceeds 256, and Python 3.8+ warns with SyntaxWarning for `is` against a literal.
Assuming `settings_backup = settings` saves a snapshot of a dict; both names point at one dict, so later edits destroy the supposed backup.
Expecting `def clear(items): items = []` to empty the caller's list; it only rebinds the parameter name and the caller's list keeps every element.
Try it yourself
Change, predict, then run
Create `original = ["a", "b"]`, then `alias = original` and `copied = original[:]`; append "c" through `alias` and "d" through `copied`, and print all three lists plus `alias is original` and `copied is original` after writing down your prediction first.
Open the Python workspaceCheck your understanding
A function is defined as `def f(items): items = items + [4]` and another as `def g(items): items += [4]`. You call each with the same list `[1, 2, 3]` (freshly created each time). What does the caller see afterwards?
- f leaves the caller's list as [1, 2, 3]; g leaves it as [1, 2, 3, 4]
- Both leave the caller's list as [1, 2, 3, 4]
- Both leave the caller's list as [1, 2, 3]
- f leaves it as [1, 2, 3, 4]; g leaves it as [1, 2, 3]
Show answer
`items + [4]` builds a brand new list object and the assignment only rebinds the local parameter name, so the caller's object is never touched. `+=` on a list calls the list's in-place add, which extends the existing object, so every name referring to it, including the caller's, sees [1, 2, 3, 4]. The tempting wrong answer is that both behave the same because `x += y` is 'shorthand' for `x = x + y`; for mutable types like lists it is not, since they implement in-place addition.