Python Memory Management and Garbage Collection
How CPython decides when memory is freed: names and references, reference counting, the garbage collector that cleans up reference cycles, weak references, measuring memory with tracemalloc, trimming objects with __slots__, and the everyday patterns that make programs grow without limit.
GloryTecks Team
Training & Placement Faculty
Last updated: 24 September 2026
Python frees memory for you, so most of the time you never think about it. Then a long-running service creeps from 200 MB to 4 GB over a week, or a data job dies with a MemoryError halfway through. Understanding the few rules CPython follows makes those problems explicable, and usually easy to fix.
Names point at objects
A Python variable is a name bound to an object, not a box containing a value. Assigning one name to another adds a second reference to the same object; del removes a name, not the object. The object is freed when nothing refers to it any more.
Reference counting does most of the work
Every object carries a count of the references to it. When the count drops to zero, CPython frees the object immediately (Python docs: gc):
import gc
import sys
import weakref
data = []
alias = data
print(sys.getrefcount(data))
del alias
print(sys.getrefcount(data))3
2The counts are one higher than you might expect because passing data to getrefcount creates a temporary reference (Python docs: sys.getrefcount). The important part is the drop when alias is deleted. Reference counting is why a file object closes and a large list's memory is released the moment the last name referring to it goes away, not at some later time.
Cycles need the garbage collector
Reference counting has one blind spot: objects that refer to each other. Neither count reaches zero, even when nothing else can reach them. CPython's cycle collector periodically finds such groups and frees them. A weak reference, which watches an object without keeping it alive (Python docs: weakref), shows the difference:
class Node:
def __init__(self, name):
self.name = name
self.partner = None
gc.collect() # start from a clean slate
a, b = Node("a"), Node("b")
a.partner, b.partner = b, a # a cycle
watcher = weakref.ref(a)
del a, b
print("freed by reference counting:", watcher() is None)
gc.collect()
print("freed by the cycle collector:", watcher() is None)freed by reference counting: False
freed by the cycle collector: TrueAfter del, the two nodes are unreachable but still alive, each held by the other. The collector's pass freed them. The collector runs automatically as objects are allocated, so you rarely call gc.collect() yourself; it matters here only to make the moment visible. Cycles are normal (parent and child objects, doubly linked lists, objects registered as each other's callbacks), but objects in cycles live longer than necessary, until the next collection.
Weak references for caches and back-links
When one object needs to refer to another without owning it, such as a child pointing back to its parent or a cache mapping IDs to objects that other code owns, a weak reference avoids keeping the target alive. weakref.WeakValueDictionary is a cache whose entries disappear automatically when the objects are no longer used anywhere else, which prevents the most common kind of cache leak.
Measure before you optimise
sys.getsizeof reports the size of one object only, not the objects it contains, so a list's size says nothing about the strings in it. To see where memory actually goes, use tracemalloc, which records allocations made by Python code and can report the current and peak totals, or the lines of code responsible for the most memory. Take a snapshot before and after the suspicious operation, compare them, and the top entries point to the culprit.
Shrinking many small objects with __slots__
Each ordinary instance stores its attributes in a per-instance dictionary. For classes with millions of instances, declaring __slots__ stores attributes in fixed slots instead and removes that dictionary (Python reference: slots):
import tracemalloc
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
class SlotPoint:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
def megabytes_for(cls, n=100_000):
tracemalloc.start()
points = [cls(i, i) for i in range(n)]
current, _ = tracemalloc.get_traced_memory()
tracemalloc.stop()
return current / 1e6
print(f"regular: {megabytes_for(Point):.1f} MB, with __slots__: {megabytes_for(SlotPoint):.1f} MB")regular: 12.8 MB, with __slots__: 8.8 MBFor 100,000 points the saving is about a third. The trade-off is flexibility: a slotted instance cannot gain attributes that are not listed. Use it for small, numerous record classes, and prefer NumPy arrays or pandas columns when the data is really a table of numbers.
Why the process does not shrink
CPython serves small objects from its own allocator, pymalloc, which carves memory into pools inside larger arenas (Python docs: memory management). Freed objects return to those pools for reuse, and an arena is handed back to the operating system only when everything in it has been freed. So after building and discarding a huge temporary structure, the process's memory often stays high even though Python can reuse the space. That is not a leak unless memory keeps growing across repeated identical work.
What actually causes memory growth
- Loading everything at once. Reading a whole file or query result into a list when it could be processed row by row; Python generators and iterators shows the lazy alternative.
- Unbounded caches. A dictionary used as a cache, or
functools.lru_cache(maxsize=None), that keeps every key ever seen. Give caches a size limit or an expiry. - Growing module-level collections, such as a global list that every request appends to "for debugging".
- References kept by accident: closures and callbacks that capture large objects, or exception tracebacks stored in variables, which keep every frame's local variables alive.
- Too many small objects. Millions of dictionaries or instances where a DataFrame, an array or slotted classes would do; see pandas performance for the categorical trick.
A useful test for a suspected leak: run the same operation a thousand times and watch whether memory levels off, which is normal, or keeps climbing, which is a leak.
Should I call gc.collect() to free memory?
Does del free memory?
How do I find what is growing in a running service?
Is Python's memory use high compared with other languages?
Lazy iteration is the most effective memory tool in everyday Python; iterators vs generators vs comprehensions measures the difference. Iterators, generators and the other advanced concepts that shape how programs use memory are covered in the Python Programming course.
Sources
- gc — Garbage Collector interface — Python documentation
- sys.getrefcount — Python documentation
- weakref — Weak references — Python documentation
- __slots__ — Python documentation
- Memory Management (C API) — Python documentation
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Python?
Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.