AnyLearn
All lessons
Programmingadvanced

CPython: the object and data model

How Python values really work under the hood: every value is a heap object with a type and a reference count, names are references not boxes, and behavior comes from the data model's dunder methods. Covers identity versus equality, attribute lookup order, descriptors, and __slots__.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

Everything is a heap object

In CPython every value, an integer, a function, a class, even a module, is a PyObject living on the heap. At minimum each one carries two fields: a type pointer (ob_type) saying what it is, and a reference count (ob_refcnt) tracking how many references point at it.

x = 1000
type(x)              # <class 'int'>
id(x)                # heap address of the object
import sys
sys.getrefcount(x)   # how many references point here

There are no primitives sitting on the stack the way there are in C or Java. 1000 is a full heap object with a header. This uniformity is the reason you can put anything in a list, pass anything to a function, and (if its type allows) give anything attributes.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. Everything is a heap object

In CPython every value, an integer, a function, a class, even a module, is a PyObject living on the heap. At minimum each one carries two fields: a type pointer (ob_type) saying what it is, and a reference count (ob_refcnt) tracking how many references point at it.

x = 1000
type(x)              # <class 'int'>
id(x)                # heap address of the object
import sys
sys.getrefcount(x)   # how many references point here

There are no primitives sitting on the stack the way there are in C or Java. 1000 is a full heap object with a header. This uniformity is the reason you can put anything in a list, pass anything to a function, and (if its type allows) give anything attributes.

2. Names are references, not boxes

A variable is a name bound to an object, not a box holding a value. Assignment never copies; it points a name at an object.

a = [1, 2, 3]
b = a          # a and b name the SAME list
b.append(4)
a              # [1, 2, 3, 4]  mutated through b

Rebinding a name (a = something_else) moves only that one name; it never touches the object or other names. This is why the classic mutable-default-argument bug bites: the default object is built once, at definition time, and shared across calls.

def add(x, bucket=[]):   # created ONCE
    bucket.append(x)
    return bucket
add(1); add(2)           # [1, 2], the same list twice

The fix is bucket=None, then if bucket is None: bucket = [] inside.

3. Identity versus equality

== asks are these equal by calling __eq__. is asks are these the same object by comparing identity. They usually agree, but not always.

a = 'hello world'
b = 'hello world'
a == b     # True (same characters)
a is b     # may be True or False

x = 257; y = 257
x is y     # often False

CPython interns small integers (-5 to 256) and many short strings, so identical literals can share one object and is returns True by accident. Outside that range you get distinct objects. Rule: never use is for value comparison, only for singletons like None (x is None). And if you define __eq__, define __hash__ too, or instances become unhashable and unusable as dict keys or set members.

4. The data model: behavior lives in dunders

An object's behavior comes from special methods (dunders) on its type, not from the interpreter special-casing values. len(x) calls type(x).__len__(x); x[k] calls __getitem__; for i in x calls __iter__; x() calls __call__; a + b calls __add__. This set of protocols is the data model, and it is what duck typing actually means: any type implementing __iter__ is iterable, with no shared base class required.

class Deck:
    def __init__(self, cards): self._c = cards
    def __len__(self):         return len(self._c)
    def __getitem__(self, i):  return self._c[i]

d = Deck(['A', 'K', 'Q'])
len(d)           # 3
d[0]             # 'A'
for c in d: ...  # works: __getitem__ gives sequence iteration

Implement the protocol and your object plugs into every built-in that expects it.

5. Types are objects too

A type is itself an object, an instance of type. So type(3) is int, and type(int) is type. type is its own type, which bottoms out the tower.

type(3)          # <class 'int'>
type(int)        # <class 'type'>
type(type)       # <class 'type'>
int.__bases__    # (<class 'object'>,)

Every class ultimately inherits from object and is an instance of type, its metaclass. Writing class Foo: ... really calls type('Foo', bases, namespace) to build the class object. A metaclass is just a type whose instances are classes; overriding it customizes class creation, not instance behavior. You rarely need custom metaclasses, but knowing that classes are ordinary objects explains why you can store them in lists, pass them to functions, and attach attributes to them.

6. How attribute access works

obj.attr is not a plain dict lookup; it runs type(obj).__getattribute__(obj, 'attr'), which follows a fixed order:

  1. Walk type(obj).__mro__ for attr. If found and it is a data descriptor (defines __set__ or __delete__), use it now.
  2. Otherwise check obj.__dict__, the instance dict.
  3. Otherwise use a class attribute from step 1 (including non-data descriptors such as plain methods).
  4. If nothing matched, call __getattr__ if the type defines one, else raise AttributeError.

So a method (a non-data descriptor) can be shadowed by an instance attribute of the same name, but a property (a data descriptor) cannot. The MRO, the method resolution order, is computed by the C3 linearization, giving one deterministic order even under multiple inheritance. Read it with Cls.__mro__.

7. Descriptors: the mechanism behind methods

A descriptor is an object whose type defines __get__ (and optionally __set__ or __delete__). When such an object lives on a class and you read it through an instance, Python calls its __get__ instead of returning it directly.

class Positive:
    def __set_name__(self, owner, name): self.n = '_' + name
    def __get__(self, obj, owner):
        if obj is None: return self
        return getattr(obj, self.n)
    def __set__(self, obj, value):
        if value <= 0: raise ValueError('must be > 0')
        setattr(obj, self.n, value)

class Account:
    balance = Positive()   # validates on every assignment

Defining __set__ makes it a data descriptor, which wins over the instance dict; defining only __get__ makes it non-data, which loses to the instance dict. This single mechanism powers property, classmethod, staticmethod, and ordinary methods: a function is a non-data descriptor whose __get__ binds self.

8. __slots__: opting out of the instance dict

By default each instance stores attributes in a per-instance __dict__: flexible, but memory-heavy. Declaring __slots__ replaces that dict with fixed, descriptor-backed C-level storage.

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

p = Point(1, 2)
p.z = 3          # AttributeError: no __dict__ to hold z
aspectdefault__slots__
storage__dict__ per instancefixed slots
memoryhighermuch lower
new attrs at runtimeallowedrejected
best forflexibilitymillions of small objects

Gotchas: a subclass without its own __slots__ regains a __dict__, quietly undoing the savings; slots also interact awkwardly with multiple inheritance. Reach for __slots__ when you have vast numbers of small, fixed-shape objects, not by default.

9. The attribute lookup path

How obj.attr resolves, in order. Data descriptors on the type win first; the instance dict comes next; non-data descriptors and class attributes come last.

flowchart TD
  A["obj.attr"] --> B["search type MRO for the name"]
  B --> C["found as data descriptor?"]
  C --> D["yes: call its __get__"]
  C --> E["no: check instance __dict__"]
  E --> F["present in instance dict?"]
  F --> G["yes: return that value"]
  F --> H["no: use class attr or non-data descriptor"]
  H --> I["still nothing?"]
  I --> J["call __getattr__ or raise AttributeError"]

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why does `x = 257; y = 257; x is y` frequently evaluate to False in CPython?
    • 257 is outside CPython's small-integer intern range, so the two literals are distinct objects
    • `is` compares values and 257 differs from 257 due to integer overflow
    • Integers are mutable, so each assignment makes a copy
    • Assigning to `y` rebinds `x` to a new object
  2. Given `def add(x, bucket=[]): bucket.append(x); return bucket`, what does the second call `add(2)` return after `add(1)`?
    • [2]
    • [1, 2]
    • [1]
    • It raises a TypeError
  3. A custom object works with the built-in `len()` when its type defines which method?
    • __count__
    • __length__
    • __len__
    • __size__
  4. On `obj.attr`, a data descriptor on the class and an instance-dict entry share the same name. Which wins?
    • The instance-dict entry always shadows the class
    • The data descriptor takes precedence
    • Whichever was assigned most recently
    • Python raises AttributeError for the conflict
  5. What does declaring `__slots__ = ('x', 'y')` on a class do?
    • Makes the attributes x and y read-only constants
    • Interns the attribute names for faster comparison
    • Caches attribute lookups in a shared dictionary
    • Replaces the per-instance __dict__ with fixed storage, disallowing new attributes

Related lessons

Programming
advanced

CPython: the GIL and free-threading

Why CPython has a Global Interpreter Lock, what it does and does not protect, and how the three concurrency models differ. Then how PEP 703 free-threading removes the GIL using per-object locks and biased, deferred, and immortal reference counting, plus the tradeoffs that keep it opt-in.

9 steps·~14 min
Programming
advanced

CPython: reference counting and the cyclic GC

How CPython reclaims memory: immediate reference counting for the common case, plus a generational cyclic garbage collector that catches the reference cycles counting cannot. Covers what changes a refcount, generations and thresholds, the gc module, __del__ and weakref, and why a process's memory does not always shrink.

9 steps·~14 min
Programming
advanced

CPython: bytecode and the interpreter loop

What actually runs when you run Python: the compiler turns source into a code object of bytecode, and a single C evaluation loop executes it on a stack machine, one frame per call. Covers code objects, the dis module, the value stack, frames, and the adaptive specializing interpreter.

9 steps·~14 min
Business
intermediate

Salesforce Objects, Fields, and Relationships

Salesforce's data model is the CRM model made concrete: standard objects like Account, Contact, Lead, Opportunity, and Case, extended by custom objects and fields. This lesson covers those objects, how lookup and master-detail relationships link them, and how lead conversion turns a raw prospect into a real relationship.

7 steps·~11 min