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
360: Understanding CPython's Memory Management
Most of the time, we treat Python's memory management like magic. You create a list, you toss it in a function, and eventually, it just disappears. But if you've ever watched a long-running process slowly eat through your server's RAM, you know that magic has limits. To fix those leaks, you have to stop thinking about "deleting variables" and start thinking about how CPython actually tracks ownership.
The delusion of the del keyword
I see this all the time with engineers coming from C++ or Rust: they use del thinking they are manually freeing memory. They'll write something like del large_dataset and assume the RAM is instantly reclaimed. In reality, del doesn't delete an object; it deletes a reference to that object. CPython uses reference counting as its primary mechanism. Every object has a counter; when that counter hits zero, the object is immediately wiped from existence.
The problem is that del only decrements that counter by one. If that object is still sitting in a list, passed into a closure, or cached in a global dictionary, the object stays alive. I've spent hours debugging "memory leaks" that were actually just hidden references in a logging module or a forgotten global list. The naive approach is to sprinkle del everywhere and hope for the best, but that's just rearranging deck chairs on the Titanic.
When reference counting fails: The Cycle
Reference counting is fast and deterministic, but it has a fatal flaw: circular references. Imagine you're building a simple parent-child relationship for a UI tree. The parent needs a list of its children, and each child needs a reference back to its parent so it can bubble up events.
class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
def add_child(self, child):
child.parent = self
self.children.append(child)
If you create a parent and a child, then del both of them from your local scope, they are still pointing at each other. Their reference counts will never hit zero. This is where CPython's Generational Garbage Collector (GC) kicks in. The GC periodically pauses your program to hunt for these "islands" of unreachable objects that are only talking to each other. It's a necessary safety net, but it's expensive. The GC has to stop the world, scan objects, and figure out what's actually garbage. If you rely solely on the GC to clean up massive circular structures, you'll see unpredictable spikes in CPU usage and latency.
Breaking the cycle with weak references
The "pro" way to handle this isn't to tweak GC thresholds or call gc.collect() manually—which is usually a sign of a deeper design flaw. Instead, you should use weakref. A weak reference is a way to point to an object without increasing its reference count. It's essentially saying, "I'd like to know where this object is, but if everyone else is done with it, feel free to kill it."
In our Node example, the parent should "own" the children (strong reference), but the child should only "observe" the parent (weak reference). By using weakref.ref, we break the cycle. The moment the parent is no longer needed by the rest of the application, it can be collected immediately, and the children's weak references will simply resolve to None. This keeps the memory footprint lean and prevents the GC from having to do the heavy lifting.
It’s a trade-off. You have to handle the possibility that the object you're referencing has been collected, which adds a tiny bit of boilerplate. But in a production system, that's a price I'm always willing to pay to avoid a 2GB memory leak in a background worker.
📋 Practical Task
Fixing a Memory Leak in a Bi-Directional Cache
You have been handed a caching system where CacheEntry objects track the CacheManager that created them. Because of the circular reference, these entries are not being cleared from memory immediately, causing the application to crash during high-load stress tests.
Your Task: Modify the provided code to use the weakref module. Ensure that the CacheEntry does not keep the CacheManager alive. You must also update the get_manager method to correctly handle the fact that the reference is now weak.
import gc
import weakref
class CacheManager:
def __init__(self):
self.entries = []
def create_entry(self, value):
entry = CacheEntry(value, self)
self.entries.append(entry)
return entry
class CacheEntry:
def __init__(self, value, manager):
self.value = value
self.manager = manager # This is the source of the leak!
def get_manager(self):
return self.manager
# Test Case
def test_leak():
manager = CacheManager()
entry = manager.create_entry("Some Data")
# We want to be able to delete the manager and have it actually disappear
del manager
del entry
gc.collect()
# In the original code, these objects might persist until the next
# full GC collection cycle or leak entirely if __del__ is overridden.
print("Cleanup complete.")
test_leak()There are no comments for now.