If you're coming from C++ or Java, Python's memory model will feel like magic. And honestly — it kind of is.

But underneath that magic is one of the most carefully engineered memory systems in any language runtime.

Let me walk you through it — from the top all the way down to the trashcan.

First: there is no "stack" for your objects

In C++, local variables live on the stack. In Python, almost everything — integers, strings, lists, your custom classes — lives on the heap. The stack only holds lightweight frames (function call state). The moment you write x = [1, 2, 3], that list is heap-allocated.

This is why Python feels "safe" — you never accidentally point to a dead stack variable.

Reference counting: the first line of defense

Every Python object carries a hidden integer: ob_refcnt.

Every time you assign, pass, or store a reference to an object, CPython calls Py_INCREF — the counter goes up. When a reference drops (variable goes out of scope, list item deleted), Py_DECREF is called — counter goes down.

When ob_refcnt hits zero → _Py_Dealloc() is called immediately. No GC pause. No waiting. The object is freed right there, in that line of code.

This is why Python's memory is so predictable for simple cases — and why with open(...) as f works reliably. The file closes the moment the block exits, because refcount drops to zero.

Cyclic GC: the second line of defense

Reference counting has one blind spot: cycles.

a = []
a.append(a)  # a refers to itself

Now a's refcount never reaches zero, even after del a. You've got a memory leak — unless something else handles it.

That something is CPython's cyclic garbage collector (gc module). It runs periodically, finds reference cycles using a mark-and-sweep variant, and breaks them. Objects with __del__ methods in cycles get special treatment — Python 3.4+ handles them correctly.

The GC tracks only "container" objects — lists, dicts, tuples, instances. Integers and strings aren't tracked. This is a deliberate performance decision.

pymalloc: Python's custom allocator

CPython doesn't call malloc() for every small object. That would be catastrophically slow. Instead it has pymalloc — a three-tier custom allocator:

Arena (256 KB) → Pool (4 KB) → Block (8–512 bytes)

Arenas are grabbed from the OS. Each arena is sliced into fixed-size pools. Each pool serves one object size class. When you create a new int or a small dict, Python grabs a pre-sized block from the right pool — no OS call needed.

This is why creating millions of small Python objects is fast. The allocator is essentially O(1) for common sizes.

Objects larger than 512 bytes bypass pymalloc and go directly to malloc().

The Trashcan: the part most people never hear about

Here's where it gets really interesting.

Imagine you build a deeply nested structure:

b = 9
while True:
    b = (b, None)  # 100,000 levels deep

Now del b. What happens?

_Py_Dealloc calls tuple_dealloc, which calls Py_XDECREF on the inner tuple, which calls _Py_Dealloc again, which calls tuple_dealloc again... 100,000 levels deep. That's 100,000 C stack frames. Stack overflow. Segfault.

CPython solves this with the Trashcan mechanism.

Inside _Py_Dealloc, before actually deallocating, it checks: "am I getting too deep?" If yes — it doesn't deallocate immediately. Instead it deposits the object into a deferred list (the trashcan) and unwinds. Once the recursion bottoms out, a separate drain loop (_PyTrash_thread_destroy_chain) walks the list and cleans up safely, one level at a time.

The "am I too deep?" check works by comparing your C stack pointer against a soft limit. Shallow enough → proceed. Too close to the edge → defer.

This is why Python can safely del a million-node linked list without blowing the stack.

The takeaway

Python's memory model, from the outside, is beautifully simple. From the inside, it's:

  • Heap-first object model
  • Immediate reference-counted deallocation
  • Cyclic GC for the cases refcounting can't handle
  • pymalloc's arena/pool/block system to avoid OS thrash
  • The trashcan to prevent deep-recursion stack overflows during cleanup

Every time you write x = None, all of this is silently working for you.

That's the kind of engineering that makes a language feel "easy."