Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
358: Memory Profiling Techniques
By the time you get to this point in your Python journey, you've likely encountered that moment where your script suddenly slows to a crawl, or worse, your OS kills the process with an OutOfMemory error. The tricky part about Python is that the garbage collector does a lot of the heavy lifting for you, but it can't save you from architectural mistakes or "leaking" references that keep objects alive longer than they should be.
How do I find the specific line of code that's eating all my RAM?
When I'm dealing with a memory spike, I don't want to guess. I want a line-by-line breakdown. For this, memory_profiler is my go-to. It's not built into the standard library, so you'll need to pip install memory_profiler, but it's worth it.
The most straightforward way to use it is the @profile decorator. You don't even need to import it in your code if you run the script via the module command. Here is a realistic scenario: imagine you're processing a large dataset and you're accidentally duplicating a list inside a loop.
# my_script.py
from memory_profiler import profile
@profile
def load_and_process_logs():
# Imagine this is a 100MB log file
logs = [f"log line {i}" for i in range(100000)]
# This is the mistake: creating a new list of the same data
# instead of processing it in place or using a generator.
processed_logs = []
for line in logs:
processed_logs.append(line.upper())
return processed_logs
if __name__ == "__main__":
load_and_process_logs()
If you run this with python -m memory_profiler my_script.py, you'll get a table showing exactly how many MiB each line added to the memory footprint. It makes the "aha!" moment happen much faster.
Why is sys.getsizeof giving me numbers that seem too small?
This is a classic trap. I've seen plenty of developers use sys.getsizeof() to debug a leak, only to find it reports a few hundred bytes for a list that contains millions of objects. The reason is that getsizeof provides the shallow size. It tells you how much space the container (the list itself) takes up, but not the objects inside that container.
If you have a list of complex custom objects, sys.getsizeof only counts the pointers to those objects. To get the deep size, you need something like the Pympler library. Here is the difference in action:
import sys
from pympler import asizeof
my_list = [[i for i in range(100)] for _ in range(100)]
print(f"Shallow size: {sys.getsizeof(my_list)} bytes")
# This only counts the outer list's pointers.
print(f"Deep size: {asizeof.asizeof(my_list)} bytes")
# This crawls through every nested list and integer.
If you're profiling a complex data structure—like a cache or a tree—always go with asizeof. Otherwise, you're just looking at the tip of the iceberg.
What's the best way to track a memory leak over time?
Line-by-line profiling is great for spikes, but for slow leaks (where memory creeps up over hours), you need a way to compare snapshots. This is where the built-in tracemalloc module shines. I love tracemalloc because it tells you not just how much memory was allocated, but exactly where that allocation happened in your source code.
The strategy here is to take a "baseline" snapshot, run your suspect code, take a second snapshot, and then calculate the difference (the delta).
import tracemalloc
# Start tracing
tracemalloc.start()
def leak_simulation():
# A global list that keeps growing—a classic leak
global leak_storage
leak_storage = []
for _ in range(1000):
leak_storage.append("I am a memory leak" * 100)
# Take the first snapshot
snapshot1 = tracemalloc.take_snapshot()
leak_simulation()
# Take the second snapshot
snapshot2 = tracemalloc.take_snapshot()
# Compare the two
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 3 Memory Increases ]")
for stat in top_stats[:3]:
print(stat)
This will output the specific file and line number responsible for the most growth. It's an absolute lifesaver when you're hunting for a global variable or a cached object that refuses to be garbage collected.
📋 Practical Task
Exercise: Debugging a Leaky Image Metadata Cache
You are maintaining a system that caches image metadata to avoid repeated disk reads. However, the system is crashing after a few hours of operation. You suspect the cache is growing indefinitely because it never evicts old entries.
Your Goal: Use tracemalloc to identify the line of code causing the memory growth and then implement a fix using a collections.deque with a maxlen to ensure the cache only keeps the most recent 100 entries.
Starting Code:
import tracemalloc
class ImageCache:
def __init__(self):
self.cache = [] # This is the suspected leak source
def add_metadata(self, image_id, data):
# Simulating adding metadata to the cache
self.cache.append({image_id: data})
def run_system():
my_cache = ImageCache()
for i in range(10000):
my_cache.add_metadata(i, "Detailed metadata string" * 10)
# TODO:
# 1. Implement tracemalloc snapshots around run_system()
# 2. Print the top memory-consuming line to confirm the leak.
# 3. Modify ImageCache to use a deque with maxlen=100.
# 4. Verify that memory usage stays constant after the fix.
There are no comments for now.