Filter concepts by levelShowing all levels.

Python · Object model

Object model

Every value is an object with an identity, a type, and a mutability — the ground truth the rest of the language builds on

  • Everything is an objecta number, a function, a class — type(x) always answers
  • Names reference objectsx = y is a second label, never a copy
  • is vs ==identity (id()) vs equality (__eq__) — different questions
  • Mutable vs immutablea property of the TYPE — decides what x += y actually does

1.Everything is an object

In Python, a number, a string, a function, a class, and a module are all objects — each has a type, an identity, and can carry attributes. There is no separate category of "primitive" value that behaves differently.

Key features

  • type(x) works on any value at all — a number, a function, a class, a module
  • A class is itself an object, of type type — which is why Name() can be called
  • Functions carry attributes too: add.__name__, add.__doc__, add.__code__
  • isinstance(5, object) is True — object sits at the root of every type
  • Because functions are objects, they can be stored in lists, passed as arguments, returned

type() applied across Python's usual categories

type() applied across Python's usual categories
Valuetype(value)
5<class 'int'>
"x"<class 'str'>
print<class 'builtin_function_or_method'>
int<class 'type'>
def f(): pass<class 'function'>
One category, no exceptions

5, "x", a function, a class

every one of these is an object

type(x)

reports which class built it — always answers something

isinstance(x, object)

always True — object is the root of every type

  1. 5, "x", a function, a class — every one of these is an object
  2. type(x) — reports which class built it — always answers something
  3. isinstance(x, object) — always True — object is the root of every type

Command

python
type(5)          # <class 'int'>
type(print)      # <class 'builtin_function_or_method'>
type(int)        # <class 'type'> — a class is an object too
isinstance(5, object)   # True, for any value at all

type(x) answers for any value — a number, a function, or a class. isinstance(x, object) is always True. There is no non-object value in Python.

2.Names vs objects, and references

A name like x is a label pointing at an object, not a container holding one. x = y makes x point at the same object y already points at — no copy is made. del x only removes the label; the object survives if another name still points at it.

Key features

  • A name is a reference to an object, stored in a namespace — never a box holding a copy
  • x = y makes x reference the SAME object y references — no copying happens
  • del x removes the name from its namespace — it does not necessarily destroy the object
  • An object can have zero, one, or many names referencing it at once
  • Passing a name to a function passes the reference, not a copy of the object

What each operation does to names vs objects

What each operation does to names vs objects
CodeEffect
x = [1, 2]creates a list object, binds the name x to it
y = xbinds a SECOND name, y, to the same object — no new list
x.append(3)mutates the one shared object — visible through y too
x = [9]rebinds x to a NEW object — y still points at the old one
del xremoves the name x — the object survives because y still references it
Two labels, one object

x = [1, 2]

one list object; name x references it

y = x

a second name, same object — no copy

del x

removes the label x — y keeps the object alive

  1. x = [1, 2] — one list object; name x references it
  2. y = x — a second name, same object — no copy
  3. del x — removes the label x — y keeps the object alive

Command

python
x = [1, 2]     # x references a new list object
y = x          # y references the SAME object — not a copy
del x          # removes the name x; the object survives via y
print(y)       # [1, 2] — unaffected by deleting x

A name references an object; it never contains one. del removes a name, not necessarily the object — it survives if another name references it.

3.Object identity

Every object has an identity — id(obj) — unique among currently-alive objects and constant for its whole lifetime. In CPython, id() happens to return the memory address, but only CPython promises that.

Key features

  • id(obj) is unique among objects alive AT THE SAME TIME, constant for obj's lifetime
  • In CPython, id(obj) returns the memory address — an implementation detail
  • A destroyed object's id can be reused by a later, unrelated object
  • is compares identity by checking id(a) == id(b) — what the is operator is built from
  • An object's identity never changes after creation, even if its value does

What id() promises, and what it does not

What id() promises, and what it does not
ClaimTrue?
Unique among objects alive right nowyes — guaranteed by the language
Stable for one object across its whole lifetimeyes — never changes while the object exists
Equal to the memory addressonly in CPython — not part of the language spec
Never reused after an object is destroyedno — a freed id can be reassigned to a new object
Comparable across two separate program runsno — meaningless outside a single running process

Command

python
id(obj)          # an int, unique among currently-alive objects
a is b            # shorthand for id(a) == id(b)
id(a) == id(b)    # the literal check "is" performs

id(obj) is unique among live objects, stable for its lifetime — a freed id can be reused, and comparing ids across runs is meaningless.

4.Equality

a == b calls a.__eq__(b) and trusts whatever it returns. A plain class inherits __eq__ from object, which falls back to identity — so two instances with equal fields are NOT equal until the class defines its own __eq__.

Key features

  • a == b calls type(a).__eq__(a, b) first — the object decides what counts as equal to it
  • object's default __eq__ falls back to identity — equivalent to is, until overridden
  • If a.__eq__(b) returns NotImplemented, Python tries b.__eq__(a) before giving up
  • Defining __eq__ without __hash__ makes a class unhashable — Python sets __hash__ to None
  • != is derived from __eq__ automatically since Python 3 — no separate __ne__ needed

Where a == b gets its answer

Where a == b gets its answer
Class definesa == b behaves like
nothing (plain object subclass)a is b — identity, by default
__eq__ comparing fieldsTrue whenever the fields compare equal, regardless of identity
__eq__ returning NotImplementedPython retries with b.__eq__(a) before falling back to False
__eq__ but no __hash__still compares correctly, but hash(a) raises TypeError

Command

python
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

a == b calls a.__eq__(b). object's default __eq__ is identity-based — a class must define its own __eq__ before equal-looking instances compare equal.

5.Mutability and immutability

Mutability is a property of an object's TYPE, not of one specific value: a list can always change in place, a tuple never can. A tuple holding a list is still immutable itself — id(t) never changes — even though what it holds can.

Key features

  • Mutability is decided by the TYPE — int, str, tuple immutable; list, dict, set mutable
  • An immutable object never changes after creation — every "edit" builds a new object
  • A tuple is immutable itself, but can hold a mutable item that still changes in place
  • Immutable objects are safe to share across threads and to use as dict keys (if hashable)
  • Mutable objects support in-place edits (.append); immutable ones return new objects

Mutable and immutable, by Python's built-in types

Mutable and immutable, by Python's built-in types
TypeMutable?Consequence
int, float, bool, str, bytesnoevery "change" (x += 1) rebinds to a new object
tuple, frozensetnonever changes itself — but a tuple can hold a mutable item
list, bytearrayyesappend/sort/etc. mutate in place — id() stays the same
dict, setyesupdate/add/etc. mutate in place — id() stays the same
Immutable container, mutable contents

The tuple itself

  • id(t) never changes
  • t[0] = x always raises TypeError
  • The tuple can never point at a different item

A mutable item inside it

  • t[1] is still the same list object
  • t[1].append(x) mutates that list in place
  • The tuple's contents (as printed) change even though it never "changed"
  • The tuple itself
    • id(t) never changes
    • t[0] = x always raises TypeError
    • The tuple can never point at a different item
  • A mutable item inside it
    • t[1] is still the same list object
    • t[1].append(x) mutates that list in place
    • The tuple's contents (as printed) change even though it never "changed"

Command

python
t = (1, [2, 3])
t[1].append(4)     # legal — mutates the list t[1] points to
t[0] = 99           # TypeError — the tuple itself cannot be reassigned

Mutability is a property of the TYPE, not the value. A tuple's own identity never changes — but a mutable object it holds can still be edited in place.

6.Hashability (the default, identity-based hash)

A plain class (no __eq__) is hashable by default, hashing by id(), not fields — two instances with identical attributes get DIFFERENT hashes. Defining __eq__ removes that default and requires a matching __hash__.

Key features

  • A plain class inherits __hash__ from object — hashing is based on id(), not fields
  • Two equal-looking plain instances hash DIFFERENTLY, because they are different objects
  • Defining __eq__ on a class sets __hash__ to None — the class becomes unhashable
  • A class needs a matching __eq__ AND __hash__ to be safe in a set or as a dict key
  • hash(obj) for the default case derives from id(obj), scrambled — not id(obj) itself

A class's hash, before and after defining __eq__

A class's hash, before and after defining __eq__
Class defineshash(instance)Two equal-field instances
nothingbased on id() — every instance differshash differently, compare unequal (is-based)
__eq__ onlyTypeError: unhashable typecompare equal, but cannot be hashed at all
__eq__ and matching __hash__based on the same fields __eq__ comparescompare equal AND hash equal

Command

python
class Plain:
    pass
hash(Plain())          # works — id-based default hash

class WithEq:
    def __eq__(self, other): ...
hash(WithEq())          # TypeError — __eq__ disabled the default hash

A plain class hashes by id() — equal-looking instances hash differently. Defining __eq__ disables that hash until a matching __hash__ is supplied.

7.Object lifecycle

An object is created by __new__ then __init__, lives as long as a reference to it exists, and is destroyed the moment its reference count hits zero — which calls __del__ if the class defines one.

Key features

  • Creation: Name(...) runs __new__ (allocates), then __init__ (sets up)
  • Life: an object stays alive exactly as long as its reference count is above zero
  • __del__(self) runs right before CPython reclaims the object — only when refcount hits zero
  • __del__ is not guaranteed to run at a predictable time — never rely on it for cleanup
  • A cyclic reference can delay destruction until the cycle-detecting garbage collector runs

An object's life, stage by stage

An object's life, stage by stage
StageWhat happens
Name(...)__new__ allocates the object; __init__ sets up its attributes
In usereference count rises and falls as names/containers reference it
Last reference gonereference count reaches zero
Destruction__del__ runs (if defined), then the memory is reclaimed
An object's life, start to end

__new__

allocates a blank object of the right class

__init__

sets up its starting attributes

referenced

stays alive as long as its refcount is above zero

__del__

runs once the refcount hits zero, right before the memory is freed

  1. __new__ — allocates a blank object of the right class
  2. __init__ — sets up its starting attributes
  3. referenced — stays alive as long as its refcount is above zero
  4. __del__ — runs once the refcount hits zero, right before the memory is freed

Command

python
class Resource:
    def __init__(self, name):
        self.name = name

    def __del__(self):
        print(f"cleaning up {self.name}")

r = Resource("db-connection")
del r   # refcount reaches 0 here -> __del__ runs

An object lives while its refcount is above zero. __del__ runs at zero — not on a schedule, and not reliably for a cycle without gc.collect().

Remember

  • Every value is an object — type(x) and isinstance(x, object) work on anything, including a class itself.
  • A name references an object; del removes the name, not necessarily the object — it survives if another name references it.
  • id(obj) is unique among currently-alive objects, stable for obj's lifetime — but a freed id can be reused later.
  • A plain class inherits an identity-based __eq__ and __hash__ — equal-looking instances are neither equal nor same-hash by default.

Pro tip

  • Mutability is a property of the TYPE — a tuple never changes itself, but can still hold a mutable item that does.
  • Defining __eq__ on a class sets __hash__ to None automatically — supply a matching __hash__ to keep it usable in a set.
  • An object's __del__ runs the instant its reference count hits zero — not on a predictable schedule otherwise.
  • Never compare id() values across two separate program runs — the numbers carry no meaning outside one process.

Next up

  • Memory management
  • Execution model

Sources

  • Data model — docs.python.org/3/reference/datamodel.html
  • id() — docs.python.org/3/library/functions.html#id
  • Every example executed on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.