Kiet Nguyen logo
NotesNotesResumeResume
© 2026 Kiet Nguyen
← All notes
Zine collage — Python object as identity, type, and value with a name binding

Breakdown·June 15, 2026·6 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.

Conceptual map for CPython (the implementation you almost certainly run). Other Pythons share the language idea with different internals. Not a full GC course, not “OOP design patterns 101.”

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:

  1. A blob of memory managed by the runtime (reference counts + cyclic GC).
  2. A type (what operations are valid—type(x) / x.__class__).
  3. An identity (id(x)—in CPython related to address; treat it as “which object”).
  4. 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.

ConceptC mental model (rough)Python mental model
VariableStorage slot of a fixed typeName bound to an object
AssignmentCopy bits into the slot (for scalars)Rebind the name to (usually) another object
TypeProperty of the variable / declarationProperty of the object
== vs identityAddress-careful comparison== value equality; is same object
Function argPass 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

ThingIs it an object?Why you care
int, float, boolYesMethods exist ((7).bit_length()); bool is a subclass of int
str, bytesYesImmutable sequences
list, dict, setYesMutation + aliasing causes surprising shared state
FunctionsYesFirst-class: pass, return, decorate
ModulesYesimport binds a name to a module object
ClassesYesCallable factories; attributes live on the class object
NoneYesSingleton; prefer x is None
Slices, code objects, framesYesDebuggers 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

MythReality
Everything is slow OOPCPython implements many objects in C; the model is objects
You must use classes for everythingFunctions and modules are objects too; classes are optional structure
is is a faster ==Different questions; wrong tool → silent bugs
Types are checked at assignmentTypes ride on objects; a name can be rebound to another type anytime
Multithreading freely uses all coresGIL (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.

Back to notes