Filter concepts by levelShowing all levels.

Python · Memory management

Memory management

How CPython allocates, tracks, frees — and lets you deliberately avoid keeping — an object in memory

  • Reference countingfreed the instant the count hits zero — not on a schedule
  • gc catches cyclesthe one case reference counting alone cannot free
  • weakrefa reference that does not keep its target alive
  • Shallow vs deep copyshares nested objects, or copies everything, recursively

1.Reference counting

Every CPython object carries a count of how many references point at it. A new name, container slot, or argument binding increments it; del, rebinding, or scope exit decrements it. The object is freed the instant the count hits zero.

Key features

  • sys.getrefcount(obj) over-reports by one — passing obj to it adds a temporary reference
  • A name binding, a container slot, and a function argument each count as one reference
  • del name, rebinding a name, and a function returning all decrement a held reference
  • The count reaching zero triggers immediate deallocation — most objects die instantly
  • Reference counting alone cannot free a cycle — mutual references never reach zero

What changes the reference count

What changes the reference count
ActionEffect on refcount
x = objincrements — a new name references it
lst.append(obj)increments — a new container slot references it
f(obj)increments for the duration of the call — the parameter is a new reference
del xdecrements — removes one reference
x = something_elsedecrements the OLD object x referenced, increments the new one
The count that decides when an object dies

obj = object()

refcount 1

alias = obj

refcount 2 — a second reference

del obj; del alias

refcount 0 — freed instantly

  1. obj = object() — refcount 1
  2. alias = obj — refcount 2 — a second reference
  3. del obj; del alias — refcount 0 — freed instantly

Command

python
import sys
sys.getrefcount(obj)   # int — includes +1 for getrefcount's own argument
del name                # decrements one reference
x = new_value            # decrements the old object's count, increments the new one's

Every object carries a refcount; freed the instant it hits zero. sys.getrefcount(obj) reports one extra, for its own reference. Alone, it cannot free a cycle.

2.Garbage collection

CPython frees most objects instantly via reference counting. The gc module adds a separate, periodic collector that finds groups of objects referencing only each other — reference counting's one blind spot — and frees them too.

Key features

  • gc.collect() runs the cycle-detecting collector now, returns how many objects it freed
  • gc.isenabled() / gc.disable() / gc.enable() control whether it runs automatically
  • It runs automatically based on allocation counts (gc.get_threshold()), not a time interval
  • It only needs to find CONTAINER objects — an int can never be part of a cycle
  • Most objects never need it — refcounting frees them the instant their count hits zero

Reference counting vs the gc module

Reference counting vs the gc module
PropertyReference countinggc (cycle collector)
Runscontinuously, on every reference changeperiodically, based on allocation thresholds
Freesany object whose count hits zerogroups of objects unreachable from outside, even mid-cycle
Handles cyclesno — a cycle never reaches zero on its ownyes — this is its entire purpose
Manual controlnone — always active, cannot be disabledgc.enable() / gc.disable() / gc.collect()
Two mechanisms, one gap

Reference counting

frees anything whose count hits zero — instantly, always on

A cycle forms

two objects only reference each other — neither count reaches zero

gc.collect()

finds unreachable cycles and frees them, periodically or on demand

  1. Reference counting — frees anything whose count hits zero — instantly, always on
  2. A cycle forms — two objects only reference each other — neither count reaches zero
  3. gc.collect() — finds unreachable cycles and frees them, periodically or on demand

Command

python
import gc
gc.collect()          # run the collector now; returns objects freed
gc.get_threshold()    # (gen0, gen1, gen2) allocation thresholds
gc.disable()           # turn off automatic runs (rarely needed)

Reference counting frees most objects instantly. gc separately, periodically finds and frees reference CYCLES — the one case it cannot handle alone.

3.Cyclic references

A reference cycle is a group of objects that reference each other in a loop, directly or through intermediaries. It occurs naturally in parent/child links, and defeats reference counting on its own.

Key features

  • The simplest cycle: a.other = b and b.other = a — two objects, one loop
  • A parent/child model is a classic source: child.parent = parent, plus parent.children
  • A cycle can involve any number of objects — three or more in a ring counts too
  • Reference counting cannot free a cycle alone — see garbage collection for what does
  • weakref breaks a cycle by design — one direction holds no real reference at all

Where cycles commonly appear

Where cycles commonly appear
PatternThe cycle
Parent/child treechild.parent = parent AND parent.children include child
Doubly linked listeach node references both .next and .prev
Observer patternsubject.observers includes observer, and observer.subject = subject
A closure capturing its own defining objectan instance method stored back onto the instance it belongs to

Command

python
import weakref

class Child:
    def __init__(self, parent):
        self.parent = weakref.ref(parent)   # breaks the cycle — no real reference back

p = Parent()
c = Child(p)
p.children.append(c)   # still a real reference, this direction only

A cycle is objects referencing each other in a loop — common in parent/child trees. Reference counting alone cannot free one; gc's collector, or a weakref, can.

4.Memory allocation and object overhead

CPython objects live in a private heap, using pymalloc — an allocator tuned for small objects (512 bytes or less). Every object carries fixed bookkeeping, so even an empty object costs more than its raw data would in C.

Key features

  • CPython manages a private heap — code never touches raw malloc()/free() directly
  • pymalloc is the default allocator for small objects (≤ 512 bytes), using fixed-size arenas
  • Every object carries fixed overhead: at minimum a reference count and a type pointer
  • sys.getsizeof(obj) reports the actual bytes an object occupies, including overhead
  • An empty list costs more than an empty tuple; both cost far more than zero

sys.getsizeof(), a few common empty/small objects

sys.getsizeof(), a few common empty/small objects
ObjectBytes
1 (a small int)28
"" (empty string)41
[] (empty list)56
() (empty tuple)40
{} (empty dict)64

Command

python
import sys
sys.getsizeof(obj)          # bytes obj occupies, including fixed per-object overhead
sys.getsizeof([1, 2, 3])    # grows with contents — but never starts at 0

Objects live in CPython's private heap; pymalloc handles small ones (≤ 512 bytes) from arenas. Every object pays fixed overhead even holding nothing.

5.Stack vs heap (conceptual model)

Every Python object lives on the heap — CPython's private heap. The call stack holds only frames and the references inside them, never the objects — unlike C, where a local can live directly on the stack.

Key features

  • In CPython, every object — an int, a string, a list, anything — lives on the heap
  • The call stack holds frames; each frame holds references (names) pointing into the heap
  • A function returning pops its frame — it does not "free stack memory" the way C does
  • An object can outlive its creating function: a closure or return value keeps it alive
  • Unlike C, Python code cannot choose to put a value directly "on the stack"

What lives where, conceptually

What lives where, conceptually
RegionHolds
Call stackframes — one per active function call
Each framelocal names, each a reference into the heap
Heapevery actual object — ints, strings, lists, functions, everything
A function returnsits frame is popped — references it held are released, objects may or may not be freed
Two regions, one direction of reference

Call stack

frames — one per active call, holding names/references only

Heap

every actual object lives here, in CPython's private heap

  1. Call stack — frames — one per active call, holding names/references only
  2. Heap — every actual object lives here, in CPython's private heap

Command

python
def f():
    x = [1, 2, 3]   # x: a name in f's frame; [1, 2, 3]: an object on the heap
    return x          # f's frame is popped; the list survives because the return value still references it

In CPython, every object lives on the heap. The call stack holds only frames and references — returning never destroys heap data like a C stack pop would.

6.Weak references

weakref.ref(obj) creates a reference that does NOT count toward obj's reference count — obj can still be freed as if it did not exist. Calling the weak reference returns the object if alive, or None if freed.

Key features

  • weakref.ref(obj) does not increment reference count — the object can be freed normally
  • Calling the weak reference — ref_obj() — returns the live object, or None once it is freed
  • Not every type supports weak references — int, str, tuple, list, dict do not by default
  • weakref.WeakValueDictionary auto-removes entries whose value has been collected
  • A weakref.proxy acts like the object itself, but raises ReferenceError once it is gone

Regular reference vs weak reference

Regular reference vs weak reference
PropertyRegular (obj = x)weakref.ref(x)
Increments refcount?yesno
Keeps the object alive?yesno
How to access the objectobj (directly)ref_obj() — call it
If the object is freednever happens while referencedref_obj() returns None
A reference that does not keep anything alive

obj = Cache()

refcount 1 — one real reference

r = weakref.ref(obj)

refcount still 1 — the weak reference does not count

del obj; r()

returns None — freed as if r never existed

  1. obj = Cache() — refcount 1 — one real reference
  2. r = weakref.ref(obj) — refcount still 1 — the weak reference does not count
  3. del obj; r() — returns None — freed as if r never existed

Command

python
import weakref
r = weakref.ref(obj)      # create a weak reference
r()                          # the live object, or None if it has been freed
weakref.WeakValueDictionary()   # a dict whose VALUES are held weakly

weakref.ref(obj) does not increment obj's reference count. Calling it returns the object while alive, None once freed — reference without keeping alive.

7.Interning

Interning is CPython reusing one object for certain repeated immutable values, instead of building a new one each time — ints -5 to 256, and some string literals. sys.intern(s) does it manually, guaranteeing equal strings share one object.

Key features

  • CPython pre-builds and reuses ints -5 to 256 — any two of these are always is-identical
  • A string that looks like an identifier, compiled as a literal, is usually interned
  • sys.intern(s) forces a string into the pool — future equal strings share one object
  • Interning is a speed optimization, never a correctness guarantee — never rely on is for it
  • A string built at runtime (concatenation, parsing) is generally NOT automatically interned

What gets interned automatically, in CPython

What gets interned automatically, in CPython
ValueInterned by default?
Small ints, -5 to 256yes — always, for the life of the process
A short identifier-like string literal ("user_id")usually — compiled as one shared constant
A string with spaces or punctuation ("hello world!")not guaranteed — depends on how it was built
Any string built at runtime (concatenation, f-string, parsing)no — a new object every time, unless interned manually

Command

python
import sys
interned = sys.intern(some_string)   # forces some_string into the shared pool
interned is sys.intern(other_string)  # True whenever other_string == some_string

Interning reuses one object for repeated immutable values — small ints and some literals automatically, any string via sys.intern(). Use == for correctness.

8.Copying objects: shallow copy vs deep copy

A shallow copy (copy.copy) builds a new outer container but reuses the SAME nested objects inside it. A deep copy (copy.deepcopy) recursively copies every nested object too, so the two structures share nothing at all.

Key features

  • copy.copy(x) — a shallow copy: new outer object, but nested objects are SHARED, not copied
  • copy.deepcopy(x) — recursively copies every nested object; nothing is shared
  • A plain assignment (y = x) is not a copy at all — it is a second name for the same object
  • For a flat structure (no nested mutable objects), shallow and deep copy behave identically
  • Some types have their own shortcut for a shallow copy: list(x), dict(x), x[:], x.copy()

Assignment vs shallow copy vs deep copy, on a nested list

Assignment vs shallow copy vs deep copy, on a nested list
OperationNew outer object?Nested objects shared?
y = xno — same object, two namesn/a — it is the same object entirely
y = copy.copy(x)yesyes — nested objects are the SAME objects
y = copy.deepcopy(x)yesno — nested objects are recursively copied too
What each copy actually duplicates

copy.copy() — shallow

  • Builds a NEW outer container
  • Nested objects are the SAME objects, referenced by both
  • Mutating a nested item is visible in both copies

copy.deepcopy() — deep

  • Builds a new outer container AND new nested objects
  • Recurses into every level
  • Nothing is shared — mutating one never affects the other
  • copy.copy() — shallow
    • Builds a NEW outer container
    • Nested objects are the SAME objects, referenced by both
    • Mutating a nested item is visible in both copies
  • copy.deepcopy() — deep
    • Builds a new outer container AND new nested objects
    • Recurses into every level
    • Nothing is shared — mutating one never affects the other

Command

python
import copy
copy.copy(x)         # shallow — new outer, shared nested objects
copy.deepcopy(x)      # deep — new outer AND new nested objects, recursively
x[:]                    # a common shortcut for a shallow copy of a list

copy.copy() shares nested objects with the original — mutating one is visible in the other. copy.deepcopy() copies everything, sharing nothing.

Remember

  • Every object carries a reference count and is freed the instant it hits zero — sys.getrefcount() always over-reports by one.
  • A reference cycle (a.other = b; b.other = a) can never be freed by reference counting alone — only gc.collect() or a weakref fixes it.
  • Every object pays fixed overhead (refcount + type pointer) even holding nothing — sys.getsizeof([]) is 56 bytes, not 0.
  • copy.copy() shares nested objects with the original; copy.deepcopy() recursively copies everything, sharing nothing.

Pro tip

  • weakref.ref(obj) does not increment refcount — pair it with at least one real reference held elsewhere, or the object is freed immediately.
  • sys.intern() guarantees two equal strings share one object — automatic interning (small ints, some literals) is an optimization, never a guarantee.
  • A parent/child object model (child.parent = parent; parent.children.append(child)) creates a reference cycle by construction — expect it.
  • Reach for deepcopy only when nested MUTABLE objects must not be shared — a shallow copy is enough, and cheaper, for flat or immutable data.

Next up

  • Execution model

Sources

  • How does Python manage memory? — docs.python.org/3/faq/design.html
  • gc — docs.python.org/3/library/gc.html
  • weakref — docs.python.org/3/library/weakref.html
  • Every example executed on Python 3.14.3 · checked 2026-08-21

Spotted a mistake on this page? Tell us.