Skip to content

Python Internals

CPython is the reference implementation of Python, written in C. It is the most widely used Implementation and the one most people mean when they say “Python.” Understanding its architecture Is essential for performance tuning, debugging segfaults in C extensions, and reasoning about Python”s memory and concurrency behavior.

The execution pipeline in CPython is:

  1. Lexer: Converts source code into tokens. Hand-written, located in Parser/tokenizer.c.
  2. Parser: Converts tokens into a Concrete Syntax Tree (CST), then into an Abstract Syntax Tree (AST). The parser is generated by a PEG (Parsing Expression Grammar) generator as of Python 3.9 (replacing the older LL(1) parser).
  3. Compiler: Walks the AST and produces bytecode. Located in Python/compile.c. The output is a code object containing bytecode instructions, constants, variable names, and metadata.
  4. Eval loop: Executes bytecode instructions one at a time in a stack-based virtual machine. Located in Python/ceval.c. This is the heart of CPython and where the GIL lives.
Source (.py)
|
v
Lexer (tokenizer.c) --> Tokens
|
v
Parser (pegen/) --> AST (asdl_c.py)
|
v
Compiler (compile.c) --> Code Object (bytecode)
|
v
Eval Loop (ceval.c) --> Results
  • PyPy: A Python interpreter written in Python (specifically, in a subset called RPython). Uses a tracing JIT compiler to generate machine code at runtime. Can be 4-5x faster than CPython for long-running programs. Trade-off: slower startup time, incomplete C extension compatibility.
  • GraalPy: Runs on the GraalVM, a polyglot VM with a JIT compiler. Good C extension compatibility via GraalVM’s Sulong runtime. Performance is competitive with PyPy for some workloads.
  • MicroPython / CircuitPython: Minimal implementations targeting microcontrollers with limited RAM (as little as 256KB). Strip out most of the standard library and use smaller internal data structures.

All implementations must conform to the Python Language Reference, but they differ in performance Characteristics, memory layout, and C extension compatibility. Code that relies on CPython-specific Behavior (reference counting timing, integer caching ranges, __dict__ implementation) may behave Differently on other implementations.

Every value in Python is an object. Not just class instances — integers, strings, functions, Modules, stack frames, and even None are objects. There are no “primitive types” in the C or Java Sense. This uniformity simplifies the language semantics but has a cost in memory and performance.

Every Python object begins with a PyObject header, defined in C as:

typedef struct _object {
Py_ssize_t ob_refcnt; // Reference count
PyTypeObject *ob_type; // Pointer to the type object
} PyObject;

On a 64-bit system, this header is 16 bytes: 8 bytes for the reference count and 8 bytes for the Type pointer. Every object, no matter how small, carries at least this overhead. A Python int Holding the value 0 consumes at minimum 28 bytes in CPython (16-byte header + 4-byte digit array + Alignment padding).

The ob_refcnt field is incremented and decremented on every assignment, function argument pass, Container insertion, and scope exit. Each increment/decrement is a thread-safe atomic operation (in Free-threaded builds) or a simple integer increment (in GIL builds). This per-operation cost is why Python is slower than C for tight loops over primitive values — a C int addition is a single Machine instruction, while a Python int addition involves:

  1. Load the left operand’s PyObject pointer.
  2. Check ob_type to confirm it is an int.
  3. Load the right operand’s PyObject pointer.
  4. Check ob_type to confirm it is an int.
  5. Extract the digit arrays from both operands.
  6. Perform the arithmetic.
  7. Allocate a new PyObject for the result.
  8. Set the result’s ob_type to &PyLong_Type.
  9. Set the result’s ob_refcnt to 1.
  10. Decrement the reference count of both operands.

The ob_type pointer enables dynamic dispatch. When Python evaluates a + bIt follows a.ob_type to the type object, then looks up the tp_as_number->nb_add slot to find the addition Function. This is a two-level indirection per operation. The tp_slots mechanism is how Python’s Data model (dunder methods) maps to C function pointers.

The type object is itself a PyObject (with its own reference count and type pointer — the type of A type is typeAnd the type of type is type). It additionally contains a large struct with Function pointers for every operation the object supports: tp_repr``tp_hash``tp_call tp_iternext``tp_as_sequence``tp_as_mappingAnd dozens more. These slots are the C-level Equivalent of dunder methods.

Reference counting is CPython’s primary memory management mechanism. Every object has a reference Count (ob_refcnt) that tracks how many references to the object exist. When the count drops to Zero, the object is immediately deallocated.

  • Assignment: a = obj increments obj.ob_refcnt.
  • Argument passing: f(obj) increments obj.ob_refcnt for the duration of the call.
  • Container insertion: lst.append(obj) or d[key] = obj increments the count.
  • Returning a reference: return obj in a function creates a new reference in the caller’s scope.
  • Name rebinding: a = new_obj decrements the old obj’s count.
  • Scope exit: when a function returns, all local variables have their reference counts decremented.
  • Container removal: lst.remove(obj) or del d[key] decrements the count.
  • del statement: del a removes the name from the current scope and decrements the count.
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2: one for 'a', one for the temporary argument to getrefcount

sys.getrefcount() returns the reference count plus one, because the call to getrefcount itself Creates a temporary reference. This makes it easy to misinterpret the output. If you see a count of 3, the actual count from your code’s perspective is 2.

Every reference count operation is a C-level increment or decrement of a Py_ssize_t. In a GIL-enabled build, this is a non-atomic operation protected by the GIL. In a free-threaded build (PEP 703, Python 3.13+), this becomes an atomic increment/decrement, which is significantly more Expensive due to cache line contention between CPU cores.

The cumulative cost of reference counting is substantial. In a tight loop that creates and discards Temporary objects (e.g., string concatenation, numeric computation), a significant fraction of CPU Time is spent on reference count manipulation rather than actual computation. This is one reason Python loops are slow relative to C loops.

Reference counting cannot reclaim objects that reference each other in a cycle:

a = []
b = []
a.append(b)
b.append(a)
del a
del b
## Both lists have refcount 1 (each other), but are unreachable from the stack

Without the cycle detector, these objects would leak permanently. CPython includes a cyclic garbage Collector (discussed below) that periodically scans for and breaks these cycles.

CPython’s garbage collector operates alongside reference counting. It is specifically designed to Handle the circular reference problem that reference counting alone cannot solve.

CPython uses a generational garbage collector with three generations:

  • Generation 0: Newly created objects. Collected most frequently.
  • Generation 1: Objects that survived one or more generation-0 collections.
  • Generation 2: Objects that survived multiple collections. Collected least frequently.

The hypothesis (and empirical observation) is that most objects die young. Newly created temporary Objects (function locals, intermediate results) are collected quickly and cheaply in generation 0. Only objects that prove their longevity (by surviving collections) are promoted to older Generations, which are scanned less often.

The gc module provides direct control over the garbage collector:

import gc
gc.collect() # Force a full collection of all generations
gc.get_count() # Current collection counts for each generation
gc.get_threshold() # Thresholds: (700, 10, 10) by default
gc.set_threshold(1000, 15, 15) # Adjust thresholds
gc.get_objects() # All objects tracked by the GC (expensive)
gc.get_referrers(obj) # Objects that reference 'obj'
gc.get_referents(obj) # Objects referenced by 'obj'

The thresholds control when each generation is collected. When the number of allocations minus Deallocations exceeds the generation-0 threshold (default 700), generation 0 is collected. If Generation 0 has been collected the threshold number of times (default 10), generation 1 is also Collected. The same logic applies for generation 2.

The GC only tracks container objects that can participate in cycles: lists, dicts, sets, instances Of user-defined classes, tuples, and similar. It does not track immutable atomic objects (integers, Strings, floats) because they cannot hold references to other objects. Note that tuples are tracked Because they can contain references to mutable objects, even though the tuple itself is immutable.

Objects with __del__ methods complicate garbage collection. The GC cannot safely break a cycle That includes objects with __del__Because breaking the cycle requires deallocating at least one Object, which triggers __del__Which might access other objects in the cycle that have already Been deallocated.

In CPython 3.4+, the GC handles this by placing such objects in a list (gc.garbage) and reporting Them as unreachable but uncollectible. The programmer must explicitly break the cycle.

import gc
class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
def __del__(self):
print(f"Deleting {self.name}")
root = Node("root")
child = Node("child")
root.children.append(child)
child.parent = root
del root
del child
gc.collect()
## The nodes are in gc.garbage because __del__ prevents cycle collection

Avoid __del__ whenever possible. Use context managers (with blocks) for resource cleanup, or Use weakref.ref callbacks, or use the atexit module. __del__ is fundamentally at odds with the Garbage collector’s ability to reclaim cyclic garbage.

The weakref module provides a way to reference an object without incrementing its reference count. A weak reference does not prevent the object from being garbage collected. When the referent is Deallocated, the weak reference returns None.

import weakref
class CacheEntry:
def __init__(self, key, value):
self.key = key
self.value = value
entry = CacheEntry("user:42", {"name": "Alice"})
ref = weakref.ref(entry)
print(ref()) # <CacheEntry object at 0x...> (the referent is alive)
del entry
import gc
gc.collect()
print(ref()) # None (the referent was collected)

weakref.WeakKeyDictionary and weakref.WeakValueDictionary are dictionary variants that use weak References for keys or values, allowing the entries to be automatically removed when the referenced Objects are garbage collected.

import weakref
cache = weakref.WeakValueDictionary()
def get_user(user_id):
if user_id not in cache:
cache[user_id] = fetch_from_db(user_id)
return cache[user_id]

This is the correct pattern for caches where you want entries to be evicted automatically when no Other code holds a reference to the cached object.

gc.disable() disables the automatic garbage collector. This can improve performance in short-lived Programs or in sections of code where you know no cycles will be created. In long-running server Processes, disabling the GC is generally a bad idea because cycles will accumulate and cause memory Leaks.

A common pattern in latency-sensitive code:

import gc
def latency_critical_section():
gc.disable()
try:
perform_work()
finally:
gc.enable()
gc.collect()

This defers GC to the end of the critical section. The finally block ensures the GC is re-enabled Even if perform_work() raises an exception.

When CPython executes a .py file, it compiles the source to bytecode and caches the result in a .pyc file (in the __pycache__ directory). On subsequent imports, CPython loads the cached Bytecode directly, skipping the lexing and parsing stages. The .pyc file contains a marshalled Code object with a magic number (for version compatibility), a timestamp, and the bytecode itself.

The compilation stages are:

  1. Source to AST: The PEG parser converts source code into an AST. You can inspect this with ast.parse().
  2. AST to bytecode: The compiler walks the AST and emits bytecode instructions. Each function, class, and module gets its own code object.
  3. Bytecode is stored in a code object alongside metadata: constant table, variable names, free variable names, cell variable names, filename, line number table, and more.
import dis
def add(a, b):
return a + b
dis.dis(add)
# 2 0 LOAD_FAST 0 (a)
# 2 LOAD_FAST 1 (b)
# 4 BINARY_ADD 0 (a)
# 6 RETURN_VALUE 1 (b)

The dis.Bytecode object provides a more programmable interface:

import dis
def example(x):
if x > 0:
return x * 2
return -x
bc = dis.Bytecode(example)
for instr in bc:
print(f"{instr.offset:4d} {instr.opname:20s} {instr.argrepr}")
# 0 LOAD_FAST x
# 2 LOAD_CONST 0
# 4 COMPARE_OP >
# 8 POP_JUMP_IF_FALSE 14
# 10 LOAD_FAST x
# 12 LOAD_CONST 1
# 14 BINARY_MULTIPLY
# 16 RETURN_VALUE
# 18 LOAD_FAST x
# 20 UNARY_NEGATIVE
# 22 RETURN_VALUE

CPython’s eval loop is a stack-based virtual machine. Most bytecode instructions either push a value Onto the stack, pop a value from the stack, or both. Binary operations like BINARY_ADD pop two Values, compute the result, and push it. LOAD_FAST pushes a local variable onto the stack. RETURN_VALUE pops the top of stack and returns it.

The stack is an array of PyObject* pointers. Stack operations are array index manipulations — Push is stack[++sp] = valuePop is value = stack[sp--]. This is extremely fast in C but still Adds overhead compared to register-based machines (like the JVM or LuaJIT) because every instruction Involves memory access to the stack array.

def greet(name, greeting="Hello"):
message = f"{greeting}, {name}!"
return message
code = greet.__code__
print(code.co_varnames) # ('name', 'greeting', 'message')
print(code.co_argcount) # 2 (positional args: name, greeting)
print(code.co_kwonlyargcount) # 0
print(code.co_consts) # (None, 'Hello', ', ', '!')
print(code.co_names) # ()
print(code.co_filename) # '<string>'
print(code.co_firstlineno) # 1
print(code.co_nlocals) # 3 (name, greeting, message)

Key attributes:

  • co_varnames: Names of local variables (including parameters).
  • co_argcount: Number of positional arguments (excluding *args).
  • co_consts: Tuple of constants used in the function (literal values loaded by LOAD_CONST).
  • co_names: Names of global variables and attributes (used by LOAD_GLOBAL``LOAD_ATTR).
  • co_freevars: Names of free variables (variables from enclosing scopes, used by closures).
  • co_cellvars: Names of cell variables (local variables referenced by nested functions).
InstructionStack EffectDescription
LOAD_FAST namepush localLoad local variable onto stack
STORE_FAST namepop to localStore top of stack into local variable
LOAD_CONST constpush constLoad a constant onto stack
LOAD_GLOBAL namepush globalLoad a global variable
LOAD_ATTR namepop obj, push attrLoad attribute from object
STORE_ATTR namepop value, pop objStore attribute on object
BINARY_ADDpop b, pop a, push a+bAddition
BINARY_MULTIPLYpop b, pop a, push a*bMultiplication
COMPARE_OP oppop b, pop a, push resultComparison (>``<``==Etc.)
POP_JUMP_IF_FALSE targetpop, jump if falseConditional jump
CALL_FUNCTION argcpop argc args, pop func, push resultFunction call
RETURN_VALUEpop and returnReturn from function
BUILD_LIST sizepop size items, push listBuild a list from stack items
GET_ITERpop iterable, push iteratorGet iterator from iterable
FOR_ITER targetpush next or jumpIterator loop

Understanding bytecode helps you write faster Python by revealing hidden costs:

import dis
# String concatenation in a loop (slow)
def slow_join(items):
result = ""
for item in items:
result += item
return result
dis.dis(slow_join)
# Each iteration: LOAD_FAST, LOAD_FAST, BINARY_ADD (creates new string), STORE_FAST
# This is O(n^2) because each BINARY_ADD copies the entire accumulated string
# Using join (fast)
def fast_join(items):
return "".join(items)
dis.dis(fast_join)
# LOAD_GLOBAL "".join, LOAD_FAST items, CALL_FUNCTION 1, RETURN_VALUE
# The join is implemented in C -- single call, O(n)

Interning is the process of deduplicating string objects so that equal strings share the same Memory. After interning, a is b is True for any two interned strings with the same value, and Dictionary lookups can use pointer comparison (a single machine instruction) instead of Character-by-character comparison.

CPython automatically interns:

  1. Identifiers: Variable names, function names, class names, attribute names. Any string that appears as a Python identifier in source code.
  2. Compile-time constants: String literals that appear in the source code, but only if they look like identifiers (letters, digits, underscores, of reasonable length). The exact rules vary by CPython version.
  3. Attribute names: The strings used as dictionary keys in __dict__ objects.

Strings that are NOT automatically interned:

  1. Strings created at runtime via concatenation, formatting, or str().
  2. Strings read from files, network, or user input.
  3. Strings created by str.join()``str.replace()Or other string methods.
a = "hello"
b = "hello"
print(a is b) # True (interned at compile time)
c = "".join(["h", "e", "l", "l", "o"])
d = "hello"
print(c is d) # False (c was created at runtime, not interned)
print(c == d) # True (value equality)

You can manually intern any string:

import sys
a = sys.intern("".join(["h", "e", "l", "l", "o"]))
b = sys.intern("hello")
print(a is b) # True (both manually interned)

Manual interning is useful in performance-critical code that does many dictionary lookups with the Same keys. After interning, the dict lookup becomes a pointer comparison rather than a full string Comparison. The trade-off is that interned strings are never garbage collected (they live in the Intern table for the lifetime of the process), so interning unbounded numbers of unique strings Causes a memory leak.

  1. Dictionary key performance: When CPython looks up a key in a dict, it first checks if the key and the stored key are the same object (a is b). If they are, the lookup is complete without comparing characters. This is the “interning shortcut” and it is a significant optimization for attribute access and global variable lookups.
  2. Memory savings: If the same string literal appears 1000 times in your source, interning ensures only one copy exists in memory.
  3. Identity comparison: a is b can replace a == b for interned strings, which is faster (but less readable and not recommended in application code).

CPython pre-allocates and caches integer objects in the range [-5, 256]. Every reference to an Integer in this range points to the same pre-allocated object.

a = 100
b = 100
print(a is b) # True
a = 257
b = 257
print(a is b) # False (most of the time)

is tests identity (pointer equality). == tests value equality (via __eq__). For integers, == Always does the right thing. is only works reliably for integers in the cached range.

a = 256
b = 256
print(a is b) # True (cached)
print(a == b) # True
a = 257
b = 257
print(a is b) # Implementation-dependent (in most cases False)
print(a == b) # True (always correct)

Never use is for numeric comparison. Always use ==. The caching range is a CPython Implementation detail, not a language guarantee. Other implementations (PyPy, GraalPy) may use Different caching strategies or no caching at all.

Small integers are extremely common in Python programs (loop counters, boolean results, short lists Indices, enum values). Caching them eliminates the overhead of allocating a new PyObject for every Occurrence. The range [-5, 256] was chosen empirically to cover the vast majority of use cases Without consuming excessive memory.

By default, every instance of a user-defined class has a __dict__ attribute — a regular Python Dictionary that stores the instance’s attributes. This dictionary is created when the instance is Allocated and grows as attributes are added.

class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.__dict__) # {'x': 1, 'y': 2}
print(type(p.__dict__)) # <class 'dict'>

The memory overhead of __dict__ is significant:

  • An empty dict in CPython 3.12 consumes approximately 64 bytes (the dict struct itself plus an empty hash table).
  • Each entry adds approximately 50-70 bytes (key pointer, value pointer, hash, and the key and value objects themselves).
  • For an object with 5 attributes, the __dict__ alone can consume 300-500 bytes on top of the base object overhead (56 bytes for the PyObject header plus instance-specific fields).

__slots__ replaces the per-instance __dict__ with a fixed set of attribute descriptors. Each Attribute is stored in a pre-allocated slot in the instance’s memory layout, similar to a C struct. This eliminates the dict overhead and reduces per-instance memory by 40-60%.

class DensePoint:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = DensePoint(1, 2)
print(p.x) # 1
p.z = 3 # AttributeError: "DensePoint'' object has no attribute "z'
print(hasattr(p, "__dict__")) # False

Memory comparison:

import sys
class Normal:
def __init__(self, x, y):
self.x = x
self.y = y
class Slotted:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
print(sys.getsizeof(Normal(1, 2))) # 56 (instance) + ~64 (dict) = ~120 bytes
print(sys.getsizeof(Slotted(1, 2))) # 56 (instance with inline slots) = 56 bytes
  1. Dynamic attribute assignment: You cannot add attributes not listed in __slots__. This is a feature, not a bug — it prevents typos and enforces a fixed schema.
  2. Default __dict__: Instances have no __dict__ unless you explicitly include "__dict__" in __slots__.
  3. Default __weakref__: Weak references to instances will not work unless you explicitly include "__weakref__" in __slots__.
class WeakRefable:
__slots__ = ("x", "__weakref__")
import weakref
obj = WeakRefable()
ref = weakref.ref(obj) # Works because "__weakref__" is in __slots__
  1. Each class in the hierarchy must define its own __slots__. If a base class omits __slots__Subclasses gain a __dict__ regardless of their own __slots__ declaration.
class Base:
pass # No __slots__ -- instances have __dict__
class Child(Base):
__slots__ = ("x",) # Ineffective! Instances still have __dict__ from Base
c = Child()
c.y = 10 # Works -- __dict__ exists from Base
  1. Slots are not inherited in the usual sense. A child class’s __slots__ only declares the new slots for that class. The parent’s slots are already part of the instance layout.
class Base:
__slots__ = ("a",)
class Child(Base):
__slots__ = ("b",)
c = Child()
c.a = 1 # OK (from Base's slots)
c.b = 2 # OK (from Child's slots)
c.c = 3 # AttributeError
  1. __slots__ is a class variable, not an instance variable. It is a tuple of strings stored on the class, and it is read by the metaclass when the class is created to configure the instance’s memory layout. You cannot set __slots__ on an instance.

The Global Interpreter Lock is a mutex that protects access to CPython’s internal state. Only the Thread holding the GIL can execute Python bytecode or manipulate Python objects. See the async chapter for a detailed treatment of the concurrency Implications. This section focuses on the implementation details.

The GIL is a pthread_mutex (on Unix) or SRWLOCK (on Windows) wrapping a simple boolean flag. The Eval loop checks this flag on every N bytecode instructions (the “check interval”). As of CPython 3.12, the default check interval is adaptive and varies based on the workload, but the historical Default was 100 bytecode instructions (or 5ms of wall clock time, whichever comes first).

In CPython 3.2+, the GIL implementation switched from a simple tick counter to a more sophisticated “time-based” mechanism that reduces contention. The requesting thread waits using a condition Variable with a timeout, and the holding thread explicitly releases the GIL when the timeout Expires. This avoids the “thundering herd” problem where multiple waiting threads all wake up and Contend for the GIL simultaneously.

The GIL is released (and later re-acquired) during:

  1. I/O operations: open()``read()``write()``socket.recv()``socket.send()And all other blocking I/O. The C extension performing the I/O releases the GIL before the blocking call and re-acquires it after.
  2. time.sleep(): Explicitly releases the GIL.
  3. C extension code: Any C extension can release the GIL using the Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS macros. NumPy, Pandas, and similar libraries do this for CPU-intensive computation.
  4. Blocking C library calls: ctypes calls to C functions release the GIL by default.
  5. subprocess``threading``multiprocessing primitives: Wait operations on locks, events, queues, and semaphores release the GIL.

The GIL is not just a performance limitation — it is deeply entangled with CPython’s design:

  1. Reference counting safety. Without the GIL, two threads could simultaneously increment or decrement the same reference count, causing race conditions. Free-threaded Python (PEP 703) solves this with atomic reference count operations, which are slower than non-atomic operations and add significant overhead to every object operation.

  2. C extension compatibility. Thousands of C extensions assume the GIL exists and do not use proper locking. Making them work in a free-threaded build requires auditing and potentially rewriting significant portions of code.

  3. Dictionary and set safety. Hash table operations in CPython are not thread-safe without the GIL. Free-threaded builds must add per-dict locks or use lock-free data structures.

  4. Frame and stack safety. The CPython eval loop operates on C-level stack frames. Without the GIL, two threads could corrupt each other’s stacks. Free-threaded builds must ensure that frame objects are properly isolated.

PEP 703 (implemented in Python 3.13 as an experimental feature) removes the GIL. Key changes:

  • Reference counts use atomic operations (slower per-operation, but enables parallelism).
  • A per-object “borrowing” mechanism reduces atomic overhead for short-lived references.
  • Many internal data structures use fine-grained locks instead of a single global lock.
  • The GIL can be optionally re-enabled via PYTHON_GIL=1 environment variable.
  • C extensions must opt in to free-threaded support via a new API.

The performance trade-off is nuanced. Single-threaded code is slightly slower (due to atomic Reference counting). Multi-threaded CPU-bound code can be significantly faster (due to true Parallelism). Multi-threaded I/O-bound code sees minimal change (I/O already released the GIL).

As of Python 3.13, the free-threaded build is experimental and not recommended for production. Many Popular packages (NumPy, Pandas, etc.) do not yet fully support it. The expectation is that by Python 3.15-3.16, free-threading will be production-ready and the GIL will be optional by default.

CPython is an interpreter, not a compiler. It reads your code, compiles it to bytecode — an intermediate representation — and then executes it one instruction at a time on a virtual machine. This virtual machine is like a stack of plates: operations push results on top and pop operands off. The GIL is a single key to a shared bathroom — only one thread can hold it, so only one thread can execute Python code at a time. Reference counting is Python’s memory janitor: every time something points to an object, the refcount goes up; every time it stops pointing, the refcount goes down. When refcount hits zero, the object is immediately cleaned up. This is fast but cannot handle circular references, which is why a periodic garbage collector exists as a backup.

1. Relying on implementation-specific object identity.

a = 256
b = 256
assert a is b # True (CPython), but not guaranteed
a = 257
b = 257
assert a is b # In most cases False, but not guaranteed

The integer caching range, string interning behavior, and tuple interning behavior are all CPython Implementation details. Write == for value comparison and is only for None checks and sentinel Objects.

2. Assuming __del__ will be called promptly.

Reference counting means __del__ is called immediately when the last reference drops — most of The time. But if the object is in a reference cycle, __del__ is not called until the garbage Collector runs, which may be much later or never (if the GC is disabled).

import gc
class Leaky:
def __del__(self):
print("cleaned up")
a = Leaky()
a.self_ref = a # Cycle
del a
# "cleaned up" is NOT printed yet
gc.collect()
# "cleaned up" is printed now (if the GC can break the cycle)

3. Using gc.get_objects() in production.

gc.get_objects() returns a list of all objects tracked by the garbage collector. In a long-running Process, this list can contain millions of objects. Converting it to a list duplicates every Reference, doubling memory usage temporarily. Use it only for debugging.

4. Misunderstanding sys.getrefcount.

import sys
x = [1, 2, 3]
print(sys.getrefcount(x))
# Returns 2, not 1: "x'' holds one reference, getrefcount"s argument holds another

The returned count is always at least 2 for a local variable: one for the variable itself, one for The temporary reference created by passing it to getrefcount.

5. Forgetting that __slots__ must be defined on every class in the hierarchy.

If any class in the MRO lacks __slots__Instances gain a __dict__Negating the memory benefit For the entire hierarchy below that class. This is a silent failure — no error is raised, but Memory usage is higher than expected.

6. Over-interning strings.

import sys
for line in open("large_file.txt"):
sys.intern(line.strip()) # Memory leak: interned strings are never freed

The intern table grows without bound. Only intern strings that are truly reusable across the Lifetime of the process, or use a bounded LRU cache as a manual alternative.

7. Assuming bytecode is stable across Python versions.

Bytecode instructions change between Python versions. Code that depends on specific opcodes (e.g., For instrumentation or analysis) will break when upgrading. Use the dis module’s abstracted Interface rather than raw opcode numbers, and pin your Python version if you depend on specific Bytecode behavior.

8. Ignoring the GC’s impact on real-time performance.

A full garbage collection (gc.collect()) can take milliseconds to seconds depending on the number Of tracked objects. In latency-sensitive applications (trading systems, game loops, real- time Signal processing), an unexpected GC pause can cause deadline misses. Profile your GC behavior with gc.get_stats() (Python 3.4+) and tune thresholds accordingly.

This topic covers the biological principles of python internals, including key concepts, experimental evidence, and real-world applications.

Key concepts include:

  • key biological principles and concepts
  • experimental methods and data analysis
  • applications of biology in medicine and industry
  • ethical considerations in biological research
  • the relationship between structure and function

Success requires the ability to recall specific factual content, apply knowledge to novel scenarios, and evaluate experimental evidence critically.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.

  • Types and Variables — The PyObject header, reference counting, and type dispatch explain why dynamic typing has a runtime cost.
  • Collections — List growth strategy, dict hash tables, and set internals are concrete applications of the memory model described here.
  • Control Flow — Bytecode instructions for loops, conditionals, and exception handling are generated from the syntax constructs in control flow.
  • Dicts, Sets, and Collections Deep Dive — Compact dict design, hash randomisation, and the slots mechanism are implementation details of the object model.