Difficulty: Advanced
Understanding Python's internal mechanisms is what separates senior developers from intermediate ones in interviews. These topics rarely affect day-to-day coding but demonstrate deep knowledge of how the language works under the hood.
The Global Interpreter Lock (GIL) is a mutex in CPython that allows only one thread to execute Python bytecode at a time. This means CPU-bound multi-threaded programs do not achieve true parallelism in CPython. The GIL exists because CPython's memory management (reference counting) is not thread-safe. For CPU-bound parallelism, you must use multiprocessing instead of threading. I/O-bound programs still benefit from threading because threads release the GIL while waiting for I/O.
Python manages memory primarily through reference counting. Every object has a reference count that tracks how many names or containers refer to it. When the count drops to zero, the memory is immediately freed. You can inspect reference counts using `sys.getrefcount()` (which itself temporarily increases the count by one because the function argument is a reference).
Reference counting alone cannot handle circular references -- when two or more objects reference each other but are no longer reachable from the program. Python's garbage collector (gc module) uses a generational approach to detect and collect these cycles. Objects are grouped into three generations: young objects are checked frequently, older objects less so. This is efficient because most objects die young.
The `__slots__` mechanism allows you to declare a fixed set of attributes for a class, replacing the default per-instance `__dict__`. This reduces memory usage significantly for classes with many instances because it uses a more compact internal structure. The trade-off is that you cannot add arbitrary attributes to instances at runtime.
In interviews, you will not be asked to write threading or gc code, but you will be asked to explain these concepts clearly and discuss their practical implications -- when to use threads vs processes, why memory leaks can still happen in Python, and when __slots__ is appropriate.
import sys
a = [1, 2, 3]
print("refcount of a:", sys.getrefcount(a))
b = a # another reference
print("refcount after b=a:", sys.getrefcount(a))
c = [a, a] # two more references inside a list
print("refcount after c=[a,a]:", sys.getrefcount(a))
del b
print("refcount after del b:", sys.getrefcount(a))
sys.getrefcount() itself adds 1 to the count (the function parameter is a reference). Creating aliases and containers increases the count, del decreases it.
class WithoutSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
obj1 = WithoutSlots(1, 2)
obj2 = WithSlots(1, 2)
print("without slots has __dict__:", hasattr(obj1, '__dict__'))
print("with slots has __dict__:", hasattr(obj2, '__dict__'))
print("obj2.x:", obj2.x, "obj2.y:", obj2.y)
try:
obj2.z = 3
except AttributeError as e:
print("slots error:", e)
__slots__ replaces the per-instance __dict__ with a fixed-size structure. You cannot add attributes not listed in __slots__, but memory usage is significantly reduced.
# The GIL prevents true CPU-bound parallelism in threads.
# This is a conceptual explanation, not a threading demo.
facts = {
"what": "Global Interpreter Lock - a mutex in CPython",
"why": "CPython's reference counting is not thread-safe",
"effect": "Only one thread executes Python bytecode at a time",
"cpu_bound": "Use multiprocessing for CPU parallelism",
"io_bound": "Threading still helps - GIL released during I/O"
}
for key, value in facts.items():
print(f"{key}: {value}")
The GIL is a CPython implementation detail. It makes single-threaded code fast but prevents multi-threaded CPU parallelism. I/O-bound code still benefits from threads.
# Conceptual demo of Python's generational garbage collection
class Node:
def __init__(self, name):
self.name = name
self.ref = None
def __repr__(self):
return f"Node({self.name})"
# Create a circular reference
a = Node("A")
b = Node("B")
a.ref = b
b.ref = a
print("a.ref:", a.ref)
print("b.ref:", b.ref)
print("circular: a.ref.ref is a:", a.ref.ref is a)
# Explain the concept
print("\nGC generations:")
print("Gen 0: new objects, checked frequently")
print("Gen 1: survived one collection")
print("Gen 2: long-lived objects, checked rarely")
print("Circular refs like a<->b need GC to collect")
Circular references cannot be freed by reference counting alone. Python's generational GC detects and collects these cycles. Objects start in gen 0 and promote to older generations if they survive.
GIL, memory management, reference counting, garbage collection, __slots__