Python Memory Leaks: Global Variables and Closures

Python manages memory through a dual mechanism consisting of reference counting and a generational cyclic garbage collector. While reference counting immediately frees objects whose reference counts reach zero, it cannot independently resolve uncollected references retained in the global scope or cycles created by closures. This article examines how the CPython runtime addresses memory retention caused by lingering global variables and circular closures, detailing the mechanics of cyclic garbage collection, the limitations of automatic cleanup, and techniques developers must use to prevent memory bloat.

How Python's Memory Management Operates

CPython utilizes two complementary systems to manage application memory:

  1. Reference Counting: Every Python object maintains a counter (ob_refcnt) tracking how many references point to it. The moment an object’s reference count falls to zero, CPython immediately deallocates its memory.
  2. Generational Cyclic Garbage Collector (GC): Because reference counting cannot identify or collect reference cycles, Python incorporates a background collector divided into three generations (Generation 0, 1, and 2). Objects that survive collection passes are promoted to older generations, which are inspected less frequently.

Lingering Global Variables: The Limits of Automation

Python does not automatically clean up lingering global variables because the interpreter treats them as actively required for the lifetime of the running module.

Why Global Variables Leak

Global variables reside in the module's __dict__ namespace (accessible via globals()). As long as the module remains loaded in memory, the module dictionary maintains an active reference to every variable within it. Consequently, their reference count never drops to zero, shielding them from both reference counting deallocation and cyclic GC passes.

Common causes include:

Handling and Remediation

Because Python's internal memory management considers global variables intentionally retained, resolution requires explicit developer action:

Circular Closures: The Role of the Cyclic Garbage Collector

Closures occur when an inner function retains access to variables from an enclosing scope. A circular closure happens when an enclosed function retains a reference to an object, and that object in turn retains a reference to the enclosed function.

How Circular Closures Form

Consider an object instance that defines an event handler or callback using a closure. If the closure captures self (or an object holding self), and self stores the closure function as an attribute, a circular reference is established:

class Handler:
    def __init__(self):
        self.callback = None

    def setup(self):
        # Closure capturing 'self'
        def on_event():
            return self.process()
        self.callback = on_event

In this scenario, self references on_event, and on_event.__closure__ contains a cell referencing self. Even if all external references to the Handler instance are discarded, the reference counts for both the instance and the closure remain at least 1.

How Python Collects Closure Cycles

The cyclic garbage collector specifically targets these unreachable reference islands through a multi-step detection algorithm:

  1. Trial Deletion: The GC identifies all container objects (objects capable of holding references, such as functions, tuples, dictionaries, and user-defined instances). It copies their reference counts into an internal field (gc_refs).
  2. Cycle Isolation: The GC iterates through these objects and decrements gc_refs for every other object they reference. If an object's gc_refs drops to zero after accounting for internal references, it is marked as part of an isolated cycle unreachable from the application's root scope.
  3. Deallocation: The GC breaks the cycle by unlinking the references and deallocates the objects, executing finalizers where present.

Since Python 3.4 (PEP 442), objects with __del__ methods caught in reference cycles can be safely collected. Python breaks cycles safely without stranding objects in gc.garbage.

Best Practices to Prevent Leaks

To maintain predictable memory footprints and reduce GC overhead: