Apply Now Apply Now Apply Now
header_logo
Post thumbnail
PYTHON

Python Memory Management: Ref Counting & Garbage Collection 

By Vishalini Devarajan

Many Python developers assume that because Python manages memory automatically, they never need to think about it. This assumption leads to memory leaks from circular references, unexpected object lifetimes, and performance bottlenecks in long-running applications. Understanding how Python allocates and frees memory gives you the tools to catch these issues before they reach production.

Table of contents


  1. TL;DR Summary
  2. How Python Allocates Memory
  3. Reference Counting: Python's Primary Memory Tool
  4. How Reference Counting Works at the C Level
  5. The Problem with Reference Counting: Circular References
  6. How the Cyclic Garbage Collector Detects Cycles
  7. Memory Optimisation: slots
  8. Common Mistakes in Python Memory Management
  9. Conclusion
  10. FAQs
    • How does Python manage memory automatically?
    • What is reference counting in Python?
    • What is a circular reference in Python and why is it a problem?
    • What is Python's garbage collector and how does it work?
    • What are Python's three garbage collection generations?
    • How do I reduce memory usage in Python?

TL;DR Summary

  • Python memory management is the process by which Python allocates, tracks, and frees memory for objects during program execution.
  • Python handles memory automatically using two primary mechanisms: reference counting and cyclic garbage collection.
  • Unlike languages like C or C++, Python developers do not manually allocate or free memory.
  • Understanding how Python memory management works under the hood helps you write more efficient code, avoid memory leaks, and debug performance issues in production applications.

Want to build deep Python expertise covering memory management, concurrency, and production-grade backend development? Explore HCL GUVI’s Python Programming Course, designed for developers ready to go beyond the basics and write efficient, scalable Python code. 

How Python Allocates Memory

Python uses a private heap to store all objects and data structures. The Python memory manager sits between the application and the operating system, handling all memory requests internally.

Python’s memory allocation works in layers:

  • The OS allocator handles raw memory pages from the operating system
  • The Python raw memory allocator manages large blocks from the OS
  • The object allocator handles small Python objects under 512 bytes using a system called pymalloc
  • Individual object allocators handle specific types like integers, strings, and lists

Read More: Garbage Collection in Python

Reference Counting: Python’s Primary Memory Tool

Reference counting is Python’s first and primary mechanism for managing memory. Every Python object maintains an internal counter that tracks how many references point to it.

When you create an object, its reference count starts at one. When you assign it to another variable, pass it to a function, or store it in a container, the count increases. When a reference goes out of scope or is reassigned, the count decreases. When the count reaches zero, Python immediately deallocates the object.

import sys

name = "GUVI"

print(sys.getrefcount(name))  # Output: 2 (one for name, one for getrefcount argument)

alias = name

print(sys.getrefcount(name))  # Output: 3

del alias

print(sys.getrefcount(name))  # Output: 2

How Reference Counting Works at the C Level

Every Python object in CPython is represented by a C struct that includes a reference count field called ob_refcnt. Two macros manage this field throughout the interpreter:

  • Py_INCREF: Increments the reference count when a new reference is created
  • Py_DECREF: Decrements the reference count and triggers deallocation if it reaches zero
x = [1, 2, 3]   # List created, refcount = 1

y = x            # refcount = 2

del x            # refcount = 1

del y            # refcount = 0 → list deallocated immediately

Want to build deep Python expertise covering memory management, concurrency, and production-grade backend development? Explore HCL GUVI’s Python Programming Course, designed for developers ready to go beyond the basics and write efficient, scalable Python code. 

💡 Did You Know?

Instagram’s engineering team once optimized the performance of its large-scale Django infrastructure by disabling Python’s cyclic garbage collector (GC) on production web servers. Because their application was carefully designed to avoid most circular references, the extra GC cycles added overhead with little benefit. The team reported a noticeable reduction in CPU usage and selectively re-enabled cyclic garbage collection only for workloads where object reference cycles were expected. This is one of the best-known real-world examples of Python memory management optimization at scale.
MDN

The Problem with Reference Counting: Circular References

Reference counting has one significant limitation. It cannot handle circular references, where two or more objects reference each other, keeping each other’s count above zero even when no external references exist.

import gc

class Node:

    def __init__(self, value):

        self.value = value

        self.next = None

# Create a circular reference

a = Node(1)

b = Node(2)

a.next = b

b.next = a  # Circular reference created

del a

del b

# Both nodes still have refcount > 0 due to the cycle

# Reference counting alone cannot free them

After del a and del b, no external code can reach either node. But a.next still points to b and b.next still points to a, so both reference counts remain at one. Reference counting sees them as live objects and never frees them.

💡 Did You Know?

Python automatically interns small integers (typically from -5 to 256) and many commonly used short strings. Instead of creating a new object each time, Python reuses the same object in memory, improving performance and reducing memory usage. For example, two variables assigned the value 100 usually reference the exact same object, which you can verify using the is operator that checks object identity rather than value equality.

How the Cyclic Garbage Collector Detects Cycles

The garbage collector uses a mark-and-sweep style algorithm specifically designed for reference cycles.

Step 1: The collector identifies all tracked container objects in a generation, such as lists, dicts, sets, and class instances.

Step 2: It computes effective reference counts by subtracting internal references between objects within the generation from their actual reference counts.

Step 3: Any object whose effective reference count drops to zero has no external references and is only kept alive by the cycle. These objects are unreachable.

Step 4: Unreachable objects are collected and their memory is freed.

import gc

gc.collect()  # Manually trigger garbage collection

print(gc.get_count())  # (gen0_count, gen1_count, gen2_count)

You can also disable the garbage collector entirely with gc.disable() if your application avoids circular references and you want to reduce GC pause overhead in latency-sensitive code.

Memory Optimisation: slots

By default, Python stores instance attributes in a dictionary called dict on each object. This is flexible but memory-intensive when you create millions of instances.

Using slots replaces the per-instance dictionary with a fixed-size array, reducing memory usage significantly.class RegularPoint:

def __init__(self, x, y):

        self.x = x

        self.y = y

class SlottedPoint:

    __slots__ = ["x", "y"]

    def __init__(self, x, y):

        self.x = x

        self.y = y

import sys

r = RegularPoint(1, 2)

s = SlottedPoint(1, 2)

print(sys.getsizeof(r))  # ~48 bytes + dict overhead

print(sys.getsizeof(s))  # ~56 bytes, but no __dict__

For data-heavy applications creating millions of small objects, slots can reduce memory consumption by 30 to 50 percent compared to regular class instances.

Common Mistakes in Python Memory Management

1. Creating unintentional circular references: The most common source of Python memory leaks is objects that reference each other, keeping each other alive indefinitely. 

2. Holding references longer than necessary: Large objects stored in module-level variables, class variables, or long-lived caches stay in memory for the lifetime of the program.

3. Not using slots for memory-intensive small objects: Add slots to any class that will be instantiated at high volume and does not require dynamic attribute assignment.

4. Misusing gc.disable() without understanding the consequences: Disabling the garbage collector speeds up code that avoids circular references but causes memory leaks the moment a cycle appears. 

5. Confusing is with == for object identity checks: Python’s integer interning and string interning can make is comparisons appear correct for small values but fail unpredictably for larger ones.

Conclusion

As Python applications scale to handle larger datasets, higher traffic, and longer runtimes, memory management moves from a background concern to a critical engineering skill. 

Understanding reference counting, cyclic garbage collection, generational collection thresholds, and practical optimisations like slots gives you the foundation to build Python applications that stay fast and stable under real production load. 

FAQs

How does Python manage memory automatically?

Python uses two mechanisms: reference counting, which tracks how many references point to each object and frees it immediately when the count reaches zero, and a cyclic garbage collector that detects and frees circular reference cycles that reference counting cannot handle.

What is reference counting in Python?

Reference counting is a memory management technique where every Python object maintains a counter of how many references point to it. 

What is a circular reference in Python and why is it a problem?

A circular reference occurs when two or more objects reference each other, keeping each other’s reference count above zero even when no external code can reach them. 

What is Python’s garbage collector and how does it work?

Python’s garbage collector is a supplementary memory management system that detects circular reference cycles. It uses generational collection, dividing objects into three generations based on age, and periodically scans each generation to find and free unreachable cyclic objects.

What are Python’s three garbage collection generations?

Generation 0 holds newly created objects and is collected most frequently. Generation 1 holds objects that survived one collection. Generation 2 holds long-lived objects and is collected least often.

MDN

How do I reduce memory usage in Python?

Use slots to eliminate per-instance dictionaries for small objects created at high volume. Break circular references using the weakref module. Delete unused large objects explicitly with del. Profile memory usage with tools like memory_profiler or tracemalloc to identify the largest consumers.

Success Stories

Did you enjoy this article?

Schedule 1:1 free counselling

Similar Articles

Loading...
Get in Touch
Chat on Whatsapp
Request Callback
Share logo Copy link
Table of contents Table of contents
Table of contents Articles
Close button

  1. TL;DR Summary
  2. How Python Allocates Memory
  3. Reference Counting: Python's Primary Memory Tool
  4. How Reference Counting Works at the C Level
  5. The Problem with Reference Counting: Circular References
  6. How the Cyclic Garbage Collector Detects Cycles
  7. Memory Optimisation: slots
  8. Common Mistakes in Python Memory Management
  9. Conclusion
  10. FAQs
    • How does Python manage memory automatically?
    • What is reference counting in Python?
    • What is a circular reference in Python and why is it a problem?
    • What is Python's garbage collector and how does it work?
    • What are Python's three garbage collection generations?
    • How do I reduce memory usage in Python?