In Python every value is an object with an identity, a type and a value. A variable is a name bound to an object. a = [1, 2]; b = a does not copy the list: both names now point at the same object, so b.append(3) changes what a sees too.
Objects are either mutable (list, dict, set, most class instances) or immutable (int, float, str, tuple, frozenset). Immutable objects can be dictionary keys because their hash never changes. Mutability is the source of most surprising Python bugs.
The classic trap is a mutable default argument: def f(x, acc=[]) creates the list once, when the function is defined, and every call shares it. Use acc=None and create the list inside the function.
Names are sticky notes. Assignment moves a sticky note onto an object; it never photocopies the object.
a = [1, 2]
b = a # same object, two names
b.append(3)
print(a) # [1, 2, 3]
print(a is b) # True: identity
c = list(a) # explicit (shallow) copy
print(c is a) # FalseGoing deeper
Function arguments are passed by assignment ('call by object reference'): the parameter becomes a new name for the caller's object. Mutating it inside the function is visible outside; rebinding it (x = something_else) is not. That single rule explains every 'why did my list change?' surprise.
Small integers and some strings are cached by CPython, so a is b can be True for equal values by accident. Never rely on it. sys.getrefcount and id() let you observe identity while learning, and copy.deepcopy handles nested structures, including cycles.
Common pitfalls
- Mutable default arguments shared across calls.
ischecks identity,==checks equality. Useisonly forNone,True,False.copy.copyis shallow: nested lists are still shared. Usecopy.deepcopywhen you need a full copy.
Best resources for this lesson
- ArticleFacts and myths about Python names and values · Ned Batchelder; the clearest explanation of this topic anywhere
- DocsPython data model reference · objects, types, and every special method