Exercises - Cyclic Garbage Collector
Understanding Cycles
Section titled “Understanding Cycles”Exercise 1 — 🟢 Beginner
Predict the reference count at each step and explain why the objects are not freed after del:
import sys
class Node: def __init__(self, value): self.value = value self.next = None
a = Node(1)b = Node(2)
print(sys.getrefcount(a)) # ?print(sys.getrefcount(b)) # ?
a.next = bb.next = a
print(sys.getrefcount(a)) # ?print(sys.getrefcount(b)) # ?
del adel b
# Questions:# 1. What is the refcount of each object after del?# 2. Are the objects freed? Why or why not?# 3. What will free them?Exercise 2 — 🟢 Beginner
Identify whether each of the following creates a circular reference:
# Case 1a = [1, 2, 3]b = [4, 5, 6]a.append(b)b.append(a)
# Case 2x = {"name": "Alice"}y = {"friend": x}
# Case 3p = []p.append(p) # ← a list that contains itself
# Case 4class A: pass
obj1 = A()obj2 = A()obj1.ref = obj2obj2.ref = obj1
# Questions for each case:# 1. Is there a circular reference?# 2. Draw the reference diagram# 3. Will reference counting alone free these objects?Exercise 3 — 🟡 Intermediate
Predict the reference counts at each step and draw the heap diagram after all del statements:
import sys
a = {}b = {}c = {}
a["next"] = bb["next"] = cc["next"] = a # closes the three-way cycle
print(sys.getrefcount(a)) # ?print(sys.getrefcount(b)) # ?print(sys.getrefcount(c)) # ?
del adel bdel c
# Questions:# 1. What is the refcount of each object after del?# 2. Draw the heap diagram showing all three objects and their references# 3. Why does none of them get freed?GC Control
Section titled “GC Control”Exercise 4 — 🟢 Beginner
Fill in the expected output and explain what each line does:
import gc
print(gc.isenabled()) # ?print(gc.get_threshold()) # ?print(gc.get_count()) # ?
gc.disable()print(gc.isenabled()) # ?
gc.enable()print(gc.isenabled()) # ?
freed = gc.collect()print(freed) # ? — what does this number represent?Exercise 5 — 🟡 Intermediate
Explain what happens to memory in this scenario — does the cyclic GC free the objects, and when?
import gc
gc.disable() # GC disabled
class Node: def __init__(self, value): self.value = value self.next = None
def create_cycle(): a = Node(1) b = Node(2) a.next = b b.next = a return None # a and b go out of scope here
create_cycle() # cycle created and lost
# Questions:# 1. Are the objects freed when create_cycle() returns?# 2. What happens because gc.disable() was called?# 3. How do you free them manually?# 4. What is the risk of leaving GC disabled?Generational Collection
Section titled “Generational Collection”Exercise 6 — 🟡 Intermediate
Fill in the expected output and explain the generational thresholds:
import gc
print(gc.get_threshold()) # ?
# Questions:# 1. When is Generation 0 collected?# 2. When is Generation 1 collected?# 3. When is Generation 2 collected?# 4. Why are older generations collected less frequently?# 5. What kind of objects end up in Generation 2?Exercise 7 — 🟡 Intermediate
Predict which generation each object ends up in and explain why:
import gc
# Scenario 1 — short lived objectdef short_lived(): x = [1, 2, 3] # created in gen 0 return None # immediately out of scope
# Scenario 2 — module level objectMY_CONFIG = { # where does this end up? "host": "localhost", "port": 8080,}
# Scenario 3 — object that survives many collectionscache = {} # where does this end up over time?for i in range(10000): cache[i] = i * 2
# Questions:# 1. Which generation does x in short_lived() end up in?# 2. Which generation does MY_CONFIG end up in over time?# 3. Which generation does cache end up in?# 4. Why does the GC scan Generation 2 so rarely?Spot the Memory Issue
Section titled “Spot the Memory Issue”Exercise 8 — 🟡 Intermediate
Identify the memory issue in this code and explain how to fix it:
class EventSystem: def __init__(self): self.listeners = []
def add_listener(self, listener): self.listeners.append(listener)
class Button: def __init__(self, event_system): self.event_system = event_system # Button holds EventSystem event_system.add_listener(self) # EventSystem holds Button
btn = Button(EventSystem())del btn
# Questions:# 1. Is there a circular reference here?# 2. Draw the reference diagram# 3. Will the objects be freed by reference counting?# 4. How would weakref solve this?Exercise 9 — 🔴 Advanced
This code builds a tree structure where each node holds a reference to its parent and its children. Identify all circular references and explain the memory implications:
import sys
class TreeNode: def __init__(self, value): self.value = value self.parent = None self.children = []
def add_child(self, child): child.parent = self # child holds reference to parent self.children.append(child) # parent holds reference to child
root = TreeNode("root")child = TreeNode("child")root.add_child(child)
print(sys.getrefcount(root)) # ?print(sys.getrefcount(child)) # ?
del rootdel child
# Questions:# 1. Draw the full reference diagram including parent and children references# 2. What are the refcounts after del?# 3. Are the objects freed?# 4. How would you restructure this to avoid the cycle?# hint: consider using weakref for the parent referenceExercise 10 — 🔴 Advanced
This code implements a simple LRU cache that unintentionally creates cycles. Identify the cycles, explain the memory implications, and propose a fix:
import sysimport gc
class CacheNode: def __init__(self, key, value): self.key = key self.value = value self.prev = None # reference to previous node self.next = None # reference to next node
class LRUCache: def __init__(self): self.cache = {} self.head = CacheNode(None, None) # dummy head self.tail = CacheNode(None, None) # dummy tail self.head.next = self.tail # head → tail self.tail.prev = self.head # tail → head ← cycle!
cache = LRUCache()del cache
# Questions:# 1. Identify all circular references in LRUCache# 2. What are the refcounts of head and tail after del cache?# 3. Draw the full reference diagram# 4. Will reference counting alone free these objects?# 5. How would you detect this with gc.collect()?Try predicting every reference count and drawing every diagram before running the code — the goal is to build a precise mental model of how Python tracks object lifetimes so that memory leaks become visible before they become production bugs.