Quick summary
Summarize this blog with AI
A name refers to an object. Rebinding a name changes that reference; mutating an object changes the object seen by every alias. Lists, dictionaries and sets are mutable. Numbers, strings and tuples are immutable, but an immutable container can still refer to mutable objects. These Python 3.12.14 fixtures make the resulting state explicit.
Distinguish mutation from rebinding
original = [1, 2]
alias = original
alias.append(3)
print(original, original is alias)
alias = [9]
print(original, alias)
number = 10
other = number
other += 1
print(number, other)
Expected output:
[1, 2, 3] True
[1, 2, 3] [9]
10 11
Assignment did not copy the list. Rebinding alias afterward did not undo the earlier append. Integers cannot change their numeric value in place, so the increment binds other to a different integer object. Do not infer these semantics from printed memory addresses or small-integer caching.
A shallow copy still shares nested objects
from copy import deepcopy
source = {"tags": ["sql"], "count": 1}
shallow = source.copy()
deep = deepcopy(source)
shallow["tags"].append("python")
shallow["count"] = 2
print(source)
print(shallow)
print(deep)
print(source["tags"] is shallow["tags"], source["tags"] is deep["tags"])
Expected output:
{'tags': ['sql', 'python'], 'count': 1}
{'tags': ['sql', 'python'], 'count': 2}
{'tags': ['sql'], 'count': 1}
True False
A shallow copy creates a new outer dictionary but retains its referenced values. deepcopy recursively copies this ordinary nested data structure; it is not a universal way to duplicate sockets, files, database sessions or arbitrary external resources. Custom classes can also customize copying. Copy only what the function’s ownership contract requires.
A tuple cannot replace a slot, but its nested list can change
record = (["sql"], "Ada")
record[0].append("python")
print(record)
try:
record[1] = "Ben"
except TypeError:
print("tuple slot assignment is forbidden")
try:
hash(record)
except TypeError:
print("tuple contains an unhashable list")
Expected output:
(['sql', 'python'], 'Ada')
tuple slot assignment is forbidden
tuple contains an unhashable list
Tuple immutability concerns its references, not a promise that everything reachable is frozen. This tuple is not hashable because one element is a list. Conversely, custom class instances can be mutable and hashable, so “mutable means unhashable” is not a universal rule; hash/equality behavior must remain suitable for dictionary and set use.
Allocate a fresh default at call time
def bad_collect(value, items=[]):
items.append(value)
return items
print(bad_collect("a"))
print(bad_collect("b"))
def collect(value, items=None):
if items is None:
items = []
items.append(value)
return items
print(collect("a"), collect("b"))
owned = []
print(collect("c", owned), owned)
Expected output:
['a']
['a', 'b']
['a'] ['b']
['c'] ['c']
The default list is created when the function definition executes, then reused. The repaired function creates a new list only when no list was supplied. It intentionally mutates a caller-provided list, including an empty one. If that mutation is undesirable, copy the supplied list instead and document whether nested elements remain shared. Do not use items = items or [] when an empty caller-owned list must be retained.
Write an ownership sentence for your function
State whether a function mutates its argument, returns a new outer object, or makes an independent deep copy of supported data. Then test both the return value and the caller’s state. Neither mutability nor immutability guarantees better speed; allocation, aliasing and workload matter. For identity tests see identity versus equality; for tuple syntax see the tuple guide.