Everything is an object
In Python, every value is an object: numbers, strings, functions, classes, modules, even
None. And a variable is not a box that holds a value. It is a name attached to an
object. Once this model is in your head, a long list of "weird" behaviours stops being weird.
Luggage tags, not boxes
Picture objects as suitcases on a carousel and names as luggage tags tied to them. a = [1, 2]
ties the tag a to a suitcase. b = a ties a second tag to the
same suitcase; it does not pack a new one. Open the suitcase through either tag and you see
the same contents. a = [9] moves the tag a to a different suitcase and leaves
the first one, and tag b, alone.
1. Every object has identity, type and value
x = [1, 2, 3]
print(id(x)) # identity: unique while the object is alive (in CPython, its memory address)
print(type(x)) # type: fixed for the object's lifetime, decides what operations mean
print(x) # value: the contents, which may change if the type is mutable
print(type(42), type("hi"), type(None), type(print), type(int))
- Identity never changes while the object exists.
id()returns it, andiscompares it. - Type never changes either. It is itself an object:
type(int)istype. - Value may or may not change. That property is called mutability, and it belongs to the type.
2. Assignment binds a name. It never copies.
a = [1, 2] b = a # a second name for the SAME list b.append(3) # mutate the object print(a, b, a is b) a = [9] # rebind the name a; the list is not touched print(a, b, a is b)
3. is versus ==
== asks "do these objects have equal values?" and calls __eq__, so each
type decides what equal means. is asks "are these the same object?" and just compares
identities. It cannot be overridden.
a = [1, 2, 3] b = [1, 2, 3] print(a == b, a is b) # equal values, two different objects c = a print(a == c, a is c) # same object, so trivially equal too x = None print(x is None) # the right way: None is a singleton
is only for singletons
Compare with is only against None, True, False, or a
sentinel object you created yourself. x is 1000 may appear to work in a quick test because
CPython caches some objects (small integers, some strings; lesson 03), but that is an implementation
detail. Python even warns about it at compile time: SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
4. Mutable and immutable types
| Immutable (value can never change) | Mutable (value can change in place) |
|---|---|
int, float, complex, bool | list |
str, bytes | dict |
tuple, frozenset | set, bytearray |
None, functions' code objects, ranges | most instances of your own classes |
"Changing" an immutable object really creates a new one and rebinds the name. id() makes that visible:
s = "cat" before = id(s) s += "s" # builds a NEW string, rebinds s print(s, id(s) == before) nums = [1, 2] before = id(nums) nums += [3] # list += extends the SAME list in place print(nums, id(nums) == before)
+= means "mutate in place if the type supports it (__iadd__), otherwise build a
new object and rebind". Same syntax, two behaviours, decided by the type. That is why the two examples
above behave differently.
5. Function arguments: "call by sharing"
Python passes references to objects. The parameter becomes a new name bound to the caller's object. A function can therefore mutate an object you pass in, but rebinding its parameter never affects the caller's name.
def add_item(basket):
basket.append("apple") # mutates the caller's list
def replace(basket):
basket = ["pear"] # rebinds the local name only
groceries = []
add_item(groceries)
replace(groceries)
print(groceries)
This is neither "pass by value" (the list was not copied) nor "pass by reference" in the C++ sense (the caller's variable could not be reassigned). The result shows exactly one apple and no pear.
6. Copies: shallow versus deep
import copy
grid = [[1, 2], [3, 4]]
shallow = copy.copy(grid) # same as grid.copy() or grid[:]
deep = copy.deepcopy(grid)
shallow[0].append(9) # mutate an inner list through the shallow copy
print("grid: ", grid) # affected: the inner list is shared
print("deep: ", deep) # unaffected
print(shallow is grid, shallow[0] is grid[0], deep[0] is grid[0])
7. Three classic traps, explained by one model
The multiplied grid
board = [[0] * 3] * 3 # ONE row object, referenced three times board[0][0] = "X" print(board) board = [[0] * 3 for _ in range(3)] # a NEW row each time round the loop board[0][0] = "X" print(board)
[row] * 3 copies references, not rows. The comprehension evaluates [0] * 3 three times and so builds three separate lists.
The tuple that both fails and succeeds
t = ([1, 2], "x")
try:
t[0] += [3]
except TypeError as e:
print("TypeError:", e)
print(t) # ...and yet the list inside DID change
t[0] += [3] runs in two steps. First t[0].__iadd__([3]) extends the list in place,
which succeeds. Then Python assigns the result back with t[0] = …, which fails because tuples
are immutable. The mutation happened before the error.
The default argument that remembers
def add_tag(tag, tags=[]): # the default list is created ONCE, when def runs
tags.append(tag)
return tags
print(add_tag("a"))
print(add_tag("b")) # same list object as last time
print(add_tag.__defaults__) # it lives on the function object
Lesson 04 covers the standard fix (tags=None) and why Python works this way.
8. Everything really is an object
import math
def greet(name):
return f"hi {name}"
greet.author = "me" # functions have attributes
say = greet # and can be bound to other names
print(say("Asha"), say.__name__, say.author)
print(type(greet), type(math), type(int), type(type))
print(isinstance(3, object), isinstance(greet, object), isinstance(int, object))
This uniformity is what makes decorators (lesson 05), functions stored in dicts, and classes passed as arguments possible. There is no special category of "things that are not objects".
Recap
- Every value is an object with an identity, a type and a value.
- Names are tags bound to objects. Assignment binds; it never copies.
==compares values,iscompares identity. UseisforNoneand sentinels only.- Mutability belongs to the type.
+=mutates lists but rebinds strings and ints. - Arguments are passed by sharing: a function can mutate your object but cannot rebind your name.
- Shallow copies share inner objects;
copy.deepcopycopies everything reachable.
Checkpoint
a = [1]; b = a; a = a + [2], what is b?
a + [2] builds a new list, and a = … rebinds a to it. b still points at the original [1]. With a += [2] instead, the list would be extended in place and b would show [1, 2].config = {"db": {"host": "a"}}, then c2 = dict(config); c2["db"]["host"] = "b". What is config["db"]["host"]?
dict(config) creates a new outer dict whose values are the same objects. Mutating the nested dict through either one is visible through both. Use copy.deepcopy(config) when nested data must be independent.if value is None: preferred over if value == None:?
None object. is checks identity directly and cannot be overridden, while == calls __eq__, which a class (NumPy arrays, for example) may define in surprising ways.