
Breakdown6 min read
Decoding “Everything is an object” in Python
Python · Systems · Learning
Python’s slogan is a runtime contract, not poetry: values are objects with identity, type, and payload; names are bindings, not typed boxes. What it means, where it is honest, and where it bends.
“Everything is an object” shows up in tutorials and then the conversation stops. This note decodes the slogan: what an object is, what a name is, where the metaphor is honest, and where it is marketing.
The slogan is a uniform value model
Taken as “everything is a Java-style class you subclass,” the phrase misleads. Taken as runtime design it is sharp:
Almost every value you can touch is a heap object with identity, type, and payload. Almost every operation goes through that object’s behavior, including integers, functions, modules, and classes themselves.
There is no second-class “naked” value tier in the language model, even when CPython cheats for speed underneath.
What “object” means here
In this context, an object is closer to:
- A blob of memory managed by the runtime (reference counts + cyclic GC).
- A type (what operations are valid—
type(x)/x.__class__). - An identity (
id(x)—in CPython related to address; treat it as “which object”). - A value / state (payload: the integer 7, the string characters, the list’s pointers, …).
name ──binds──► object
│
├── identity (is / id)
├── type (type / behavior)
└── value (== / contents)
That is why people also say “names reference objects.” Assignment does not fill a typed box; it reattaches a label.
| Concept | C mental model (rough) | Python mental model |
|---|---|---|
| Variable | Storage slot of a fixed type | Name bound to an object |
| Assignment | Copy bits into the slot (for scalars) | Rebind the name to (usually) another object |
| Type | Property of the variable / declaration | Property of the object |
== vs identity | Address-careful comparison | == value equality; is same object |
| Function arg | Pass by value (bits / pointer) | Pass a reference to an object (rebinding vs mutating still differs) |
Two names can bind the same object. Mutating through one name is visible through the other. That single fact explains half of “Python is weird” bugs.
The three facts, with code
1. Identity — which object
a = []
b = a
c = []
a is b # True — same object
a is c # False — equal shape, different object
a == c # True — same value (empty lists)
Use is for singletons (None, and sentinels you create). Use == for value equality. Do not use is to compare integers or strings: interning is an implementation detail.
2. Type — what behavior
x = 7
type(x) # <class 'int'>
isinstance(x, int) # True
Types are objects too (type is a type; classes are instances of type). Decorators, metaclasses, and “class as factory” fall out of that—advanced, but not a separate universe.
3. Value — payload
Mutable objects can change payload in place (same identity). Immutable objects force new objects when you “change” them:
s = "hi"
print(id(s))
s = s + "!" # rebind name s to a new str object
print(id(s)) # different id
xs = [1, 2]
print(id(xs))
xs.append(3) # same list object, value changed
print(id(xs)) # same id
“Everything” includes things people forget
| Thing | Is it an object? | Why you care |
|---|---|---|
int, float, bool | Yes | Methods exist ((7).bit_length()); bool is a subclass of int |
str, bytes | Yes | Immutable sequences |
list, dict, set | Yes | Mutation + aliasing causes surprising shared state |
| Functions | Yes | First-class: pass, return, decorate |
| Modules | Yes | import binds a name to a module object |
| Classes | Yes | Callable factories; attributes live on the class object |
None | Yes | Singleton; prefer x is None |
| Slices, code objects, frames | Yes | Debuggers and dis touch them |
If it can appear on the right-hand side of assignment or as a function argument in normal code, treat it as an object.
Four places the idea shows up every day
1. Assignment and arguments (aliasing)
def append_one(bucket):
bucket.append(1)
data = []
append_one(data)
# data is [1] — function received a reference to the same list
Python always passes a reference. Whether the callee affects the caller depends on mutation vs rebinding:
def rebind(bucket):
bucket = [1] # local name only; caller unaffected
def mutate(bucket):
bucket[:] = [1] # mutates the object the caller still holds
2. Default arguments (the classic trap)
Defaults are evaluated once, at function definition time, and stored on the function object. A default list is one object shared across calls:
def add(item, bucket=[]): # bug: shared object
bucket.append(item)
return bucket
Fix: default None, create a new list inside. This is not a syntax quirk—it is the object model biting you.
3. Shared mutable configs
BASE = {"retries": 3, "hosts": []}
cfg = BASE
cfg["hosts"].append("lab-01")
# BASE is also mutated — you aliased a shared dict
When you mean “a copy of this data,” copy deliberately (copy.copy / copy.deepcopy when nested) or prefer immutable structures (tuple, frozen dataclasses).
4. Methods and uniform operations
(255).bit_count()
"sensor".upper()
[1, 2, 3].count(2)
The sugar x.method() means: look up the attribute on the object / its type and call it. Special methods (__eq__, __len__, __iter__) are how ==, len(), and for hook into the same model.
What the slogan does not mean
| Myth | Reality |
|---|---|
| Everything is slow OOP | CPython implements many objects in C; the model is objects |
| You must use classes for everything | Functions and modules are objects too; classes are optional structure |
is is a faster == | Different questions; wrong tool → silent bugs |
| Types are checked at assignment | Types ride on objects; a name can be rebound to another type anytime |
| Multithreading freely uses all cores | GIL (CPython): one thread runs Python bytecode at a time—related history, separate design choice |
CPython honesty (enough to stay grounded)
Under the hood, CPython uses PyObject headers (refcount, type pointer). Small integers and some strings may be interned so is looks true for equal values. Do not rely on that. The language guarantee is the abstract object model; interning is an optimization.
Garbage collection is mainly reference counting, plus a cycle detector (a list that contains itself). Lingering names in globals, caches, or closures keep objects alive. Unexpected growth is often those names, not a missing free.
Mini lab
# 1) Alias
a = b = []
a.append(1)
assert b == [1]
# 2) Rebind vs mutate
def rebind(x):
x = x + [9]
def mutate(x):
x.append(9)
s = [1]
rebind(s)
assert s == [1]
mutate(s)
assert s == [1, 9]
# 3) Defaults
def f(x, acc=None):
if acc is None:
acc = []
acc.append(x)
return acc
assert f(1) == [1]
assert f(2) == [2] # new list each call; a default [] would have been [1, 2]
# 4) Identity vs equality
assert [1] == [1] and [1] is not [1]
assert None is None
If any assert fails in your head before running, re-read the three-facts section.
Closing
See the graph (aliasing, mutation, rebinding) and half the “mystery” is mechanics. The slogan is a contract, not physics: check behavior instead of quoting stickers.