Skip to main content

How to Explicitly Free Memory in Python

Python manages memory automatically through garbage collection and reference counting, so most of the time you never need to think about freeing memory manually. However, there are scenarios - working with large datasets, running long-lived services, or building performance-critical applications - where understanding how to explicitly release memory can make a real difference.

This guide explains how Python's memory management works under the hood and walks you through practical techniques to explicitly free memory when the automatic mechanisms aren't enough.

How Python Memory Management Works

Before learning to free memory, it helps to understand how Python allocates and reclaims it:

  1. Reference counting - every Python object has a reference count. When the count drops to zero (no variables or data structures point to the object), the memory is immediately reclaimed.
  2. Cyclic garbage collector - reference counting alone can't handle circular references (e.g., object A references object B, which references object A). Python's gc module periodically detects and cleans up these cycles.
  3. Memory allocator - Python uses its own memory allocator (pymalloc) for small objects, which pools memory blocks for efficiency. Freed memory may not always be returned to the operating system immediately.

When Should You Explicitly Free Memory?

In most Python programs, explicit memory management is unnecessary. However, consider it in these situations:

  • Large datasets - loading millions of rows or processing large files that consume significant RAM.
  • Long-running processes - web servers, daemons, or background workers that run for days or weeks and gradually accumulate memory.
  • Memory-constrained environments - embedded systems, containers with strict memory limits, or serverless functions.
  • Batch processing pipelines - processing data in stages where earlier stages produce large intermediate results that are no longer needed.

Technique 1: Using the del Statement

The del statement removes a reference to an object. If no other references exist, the object becomes eligible for garbage collection and its memory is reclaimed.

import sys

my_list = [i for i in range(1_000_000)]
print(f"Before del: {sys.getsizeof(my_list):,} bytes")

del my_list

# my_list no longer exists
try:
print(my_list)
except NameError as e:
print(f"After del: {e}")

Output:

Before del: 8,448,728 bytes
After del: name 'my_list' is not defined
caution

del removes the variable name, not the object itself. If other variables still reference the same object, the memory is not freed:

a = [1, 2, 3]
b = a # b references the same list
del a # Only removes the name 'a'

print(b) # The list still exists via 'b'

Output:

[1, 2, 3]

The list's memory is only reclaimed when all references are removed.

Technique 2: Setting Variables to None

Assigning None to a variable breaks its reference to the original object, making that object eligible for garbage collection (if no other references exist). Unlike del, the variable name still exists.

data = [i ** 2 for i in range(500_000)]
print(f"Data length: {len(data)}")

# Break the reference
data = None

print(f"Data is now: {data}")

Output:

Data length: 500000
Data is now: None

This approach is useful inside functions or loops where you want to release a large object but keep the variable available for reassignment later.

Technique 3: Clearing Data Structures In-Place

For mutable data structures like lists, dictionaries, and sets, you can release their contents without deleting the variable itself using built-in methods:

import sys

# List
my_list = list(range(1_000_000))
print(f"List before clear: {sys.getsizeof(my_list):,} bytes")
my_list.clear()
print(f"List after clear: {sys.getsizeof(my_list):,} bytes")

# Dictionary
my_dict = {i: i * 2 for i in range(100_000)}
print(f"\nDict before clear: {sys.getsizeof(my_dict):,} bytes")
my_dict.clear()
print(f"Dict after clear: {sys.getsizeof(my_dict):,} bytes")

Output:

List before clear: 8,000,056 bytes
List after clear: 56 bytes

Dict before clear: 5,242,960 bytes
Dict after clear: 64 bytes
Data StructureMethod
listmy_list.clear()
dictmy_dict.clear()
setmy_set.clear()
dequemy_deque.clear()

Technique 4: Forcing Garbage Collection with gc.collect()

Python's garbage collector runs automatically, but you can trigger it manually using the gc module. This is particularly useful for reclaiming memory from objects involved in circular references.

import gc

class Node:
def __init__(self, name):
self.name = name
self.ref = None

# Create a circular reference
a = Node("A")
b = Node("B")
a.ref = b
b.ref = a # Circular reference: A -> B -> A

# Remove variable references
del a
del b

# Reference counting alone can't free these. force garbage collection
collected = gc.collect()
print(f"Garbage collector freed {collected} objects")

Output:

Garbage collector freed 2 objects

Checking for Uncollectable Objects

You can inspect what the garbage collector finds:

import gc

gc.set_debug(gc.DEBUG_STATS)

# Force a full collection
gc.collect()

This prints statistics about the garbage collector's activity, helping you identify memory leaks.

tip

In long-running applications, you can schedule periodic gc.collect() calls during idle periods to keep memory usage under control without impacting performance during critical operations.

Technique 5: Using Context Managers for Resource Cleanup

Context managers (with statements) ensure that resources like files, database connections, and network sockets are properly closed and released, even if an exception occurs.

# ✅ Correct: file is automatically closed and memory is released
with open("large_file.txt", "r") as file:
data = file.read()
# Process data...

# 'file' is closed here, resources are released

Common Mistake: Forgetting to Close Resources

# ❌ Wrong: file handle stays open, consuming memory
file = open("large_file.txt", "r")
data = file.read()
# file.close() is never called if an exception occurs

Always prefer with statements for any resource that needs cleanup. You can also create custom context managers for your own objects:

class LargeBuffer:
def __enter__(self):
self.buffer = bytearray(100_000_000) # ~100 MB
print(f"Allocated {len(self.buffer):,} bytes")
return self.buffer

def __exit__(self, exc_type, exc_val, exc_tb):
del self.buffer
print("Buffer released")

with LargeBuffer() as buf:
print(f"Buffer size: {len(buf):,} bytes")

print("Outside the context manager")

Output:

Allocated 100,000,000 bytes
Buffer size: 100,000,000 bytes
Buffer released
Outside the context manager

Technique 6: Processing Data in Chunks

Instead of loading an entire dataset into memory, process it incrementally using generators, iterators, or chunked reading:

def process_large_file(filepath):
"""Read and process a file line by line instead of loading it all."""
line_count = 0
with open(filepath, "r") as file:
for line in file: # Reads one line at a time
line_count += 1
# Process line...
return line_count

# Only one line is in memory at a time, regardless of file size

For libraries like pandas, use the chunksize parameter:

import pandas as pd

# Process a large CSV in chunks of 10,000 rows
for chunk in pd.read_csv("large_dataset.csv", chunksize=10_000):
# Process each chunk
print(f"Processing {len(chunk)} rows...")
# chunk is automatically replaced in the next iteration
tip

Generators and chunked processing are often more effective than explicit memory freeing because they prevent large allocations from happening in the first place.

Complete Example: Combining Multiple Techniques

Here's a practical example that demonstrates several memory management techniques working together:

import gc
import sys

def process_data():
# Step 1: Create a large dataset
print("Step 1: Creating large dataset...")
large_data = [i ** 2 for i in range(2_000_000)]
print(f" Memory used: {sys.getsizeof(large_data):,} bytes")

# Step 2: Extract what we need
print("Step 2: Extracting summary...")
total = sum(large_data)
count = len(large_data)

# Step 3: Free the large dataset. We only need the summary
print("Step 3: Freeing large dataset...")
del large_data
gc.collect()

# Step 4: Continue with minimal memory usage
average = total / count
print(f"Step 4: Average = {average:,.2f}")
print(f" Total items processed: {count:,}")

process_data()

# After the function returns, all local variables are freed automatically
collected = gc.collect()
print(f"\nFinal cleanup: {collected} objects collected")

Output:

Step 1: Creating large dataset...
Memory used: 17,128,280 bytes
Step 2: Extracting summary...
Step 3: Freeing large dataset...
Step 4: Average = 1,333,332,333,333.50
Total items processed: 2,000,000

Final cleanup: 0 objects collected

Quick Reference: When to Use Each Technique

TechniqueUse When
del variableYou're done with a large object and want to remove the name entirely
variable = NoneYou want to release the object but keep the variable for later use
.clear()You want to empty a collection but reuse the container
gc.collect()You suspect circular references or need immediate cleanup
Context managers (with)Working with files, connections, or any resource needing cleanup
Chunked processingDealing with data too large to fit in memory at once

Conclusion

Python's automatic garbage collection handles memory management effectively in most situations. When you do need to take control, Python provides several tools:

  • del and None assignment break references, making objects eligible for collection.
  • .clear() empties data structures in place, immediately releasing their contents.
  • gc.collect() forces the garbage collector to run, reclaiming memory from circular references.
  • Context managers ensure resources are properly cleaned up, even when errors occur.
  • Chunked processing avoids large memory allocations altogether.

The most effective memory management strategy is often prevention - processing data in chunks, using generators, and structuring code so large objects go out of scope naturally. Reserve explicit memory freeing for the cases where you're working with genuinely large data or long-running processes where every megabyte counts.