The Python runtime — interpreter, bytecode, and startup

Where CPython lives on disk, what happens on import, and how startup time is spent.

🧰 Module 0 8 min read Not started

Why this matters

Most developers treat Python as a magic box that runs print("hello"). Senior engineers know what happens when you press enter: the source is parsed, compiled to bytecode, executed by the CPython VM, with the GIL gating thread execution. Understanding this prevents the embarrassing "why is my multithreaded code not faster?" moment and unlocks the language's escape hatches (C extensions, numba, threading vs multiprocessing, the new free-threaded build).

Learning objectives

  1. Name the implementations of Python and pick between them.
  2. Explain the source → bytecode → execution pipeline.
  3. Understand objects, references, and the garbage collector.
  4. Explain the GIL and Python 3.13's free-threaded build.
  5. Read a dis disassembly.

1. Implementations of Python

NameNotes
CPythonReference implementation; what you have unless told otherwise. C-based.
PyPyJIT-compiled; often 5-10× faster on pure-Python; lags on the latest version a year.
MicroPython / CircuitPythonFor microcontrollers. Subset of stdlib.
Jython / IronPythonJVM / .NET hosts. Mostly dead; .NET has stronger alternatives.
GraalPyOracle's GraalVM Python; polyglot.

The rest of this course assumes CPython 3.12+.


2. The pipeline: source → bytecode → VM

my_script.py  ──►  Parser  ──►  AST  ──►  Compiler  ──►  bytecode (.pyc cached)
                                                                    │
                                                                    ▼
                                                          CPython VM (eval loop)
                                                                    │
                                                                    ▼
                                                                 Output

Source → AST

The parser (since 3.9, a PEG parser) turns text into an Abstract Syntax Tree:

python
import ast
tree = ast.parse("x = 1 + 2")
print(ast.dump(tree, indent=2))

You can inspect, transform, and recompile ASTs — the basis of linters, formatters, and tools like pytest (which rewrites assert statements).

AST → bytecode

python
import dis
def f(x, y):
    return x + y * 2
dis.dis(f)

Output (abridged):

  3       RESUME      0
          LOAD_FAST   x
          LOAD_FAST   y
          LOAD_CONST  2
          BINARY_OP   5 (*)
          BINARY_OP   0 (+)
          RETURN_VALUE

Bytecode is a stack-based instruction set. The CPython evaluation loop is a giant switch over these opcodes (literally — see ceval.c).

Cached as .pyc

CPython writes compiled bytecode to __pycache__/*.pyc so it doesn't re-compile next time. Safe to delete; the next import regenerates.


3. Everything is an object

In Python, literally everything is an object — integers, functions, classes, modules.

python
x = 42
print(type(x))               # <class 'int'>
print(id(x))                 # CPython: memory address
print(x.__class__.__mro__)   # method resolution order

int, str, list are all C structs (PyObject*) under the hood. Each carries a header with:

  • a refcount (ob_refcnt),
  • a type pointer (ob_type),
  • type-specific data.

Reference counting

CPython manages memory primarily with reference counts: when refcount == 0, the object is freed immediately. Predictable destruction (__del__ fires deterministically — unlike Java's finalize).

python
import sys
x = []
print(sys.getrefcount(x))      # at least 2 (one local, one for the arg)

Cyclic garbage collector

Refcounting can't free cycles (a.ref = b; b.ref = a). A periodic GC sweep finds and frees them.

python
import gc
gc.collect()                   # force a pass
gc.set_threshold(700, 10, 10)  # tune

For most code, you never touch gc. Long-lived web servers occasionally disable it for latency reasons.


4. Names, references, immutability

Python has names, not variables in the classical sense. A name is a label pointing to an object.

python
a = [1, 2, 3]
b = a              # b points to the SAME list
b.append(4)
print(a)           # [1, 2, 3, 4]   surprise!

Compared to:

python
a = (1, 2, 3)
b = a              # both point to the same tuple
# Cannot mutate; tuples are immutable.
MutableImmutable
list, dict, set, custom classesint, float, str, tuple, frozenset, bytes

The pass-by-value vs pass-by-reference debate is wrong in Python: it's pass-by-object-reference. The function receives the same reference; whether it can mutate depends on the object's mutability.

The small-int / interned-string trap

CPython caches small integers (-5 to 256) and short identifier-like strings:

python
a, b = 100, 100
print(a is b)           # True
a, b = 1000, 1000
print(a is b)           # False (in normal scripts)

Use == to compare values, is to compare identity. Almost always use ==.


5. Scopes — LEGB

Python looks up names in this order:

  1. Local: current function.
  2. Enclosing: outer functions (closures).
  3. Global: module-level.
  4. Built-in: len, print, etc.
python
x = "global"
def outer():
    x = "enclosing"
    def inner():
        # x = "local"   # uncomment to shadow
        print(x)
    inner()
outer()                   # "enclosing"

To modify an enclosing/global name, use nonlocal / global:

python
def make_counter():
    count = 0
    def inc():
        nonlocal count
        count += 1
        return count
    return inc

6. The Global Interpreter Lock (GIL)

CPython's evaluator holds a single mutex — the GIL — while running bytecode. Only one thread runs Python at a time.

Implications:

  • CPU-bound multithreading does not speed up pure-Python code.
  • I/O-bound multithreading does speed up (the GIL releases during blocking I/O).
  • For CPU-bound parallelism, use multiprocessing or compiled extensions (NumPy, PyTorch, native C) which release the GIL.

The future: free-threaded Python

Python 3.13 ships an experimental no-GIL build (PEP 703). Build with --disable-gil; binaries are slightly slower single-threaded but scale across cores for pure-Python CPU work. Python 3.14 continues stabilising this. By 2027 expect it to become default; until then, treat it as a compile-time flag.

Detect at runtime (3.13+):

python
import sys
print(sys.version)
print(sys._is_gil_enabled())     # True on standard build

In this course's Phase 7 we cover when free-threaded helps (and when async or multiprocessing is still better).


7. Built-in types you should know cold

python
# Numbers
1, 1.0, 1 + 2j           # int, float, complex
0xff, 0b1010, 0o17       # hex, binary, octal
1_000_000                # numeric separators

# Booleans (subclasses of int)
True, False
isinstance(True, int)    # True

# Strings (immutable, unicode)
"hello", 'world', """multi
line""", r"raw\n", f"x={1+2}"

# Bytes (immutable) and bytearray (mutable)
b"\x00\xff", bytearray(b"\x00\xff")

# Sequences
[1, 2, 3]                # list
(1, 2, 3)                # tuple
range(10)                # lazy integer range

# Mappings and sets
{"a": 1, "b": 2}         # dict (insertion-ordered since 3.7)
{1, 2, 3}                # set
frozenset({1, 2})        # immutable set

# None and sentinels
None

8. The REPL and IPython

The REPL (python or uv run python) is your fastest feedback loop. Use it constantly.

For a better experience:

powershell
uv add --dev ipython
uv run ipython

Features: tab completion, ?obj for docs, ??obj for source, %timeit for micro-bench, %run for scripts, magic for shell commands (!ls).

For data work, Jupyter (uv add jupyterlab; uv run jupyter lab) opens a notebook UI in your browser.


9. Reading a stack trace

python
def divide(a, b):
    return a / b
def calculate():
    return divide(1, 0)
calculate()
Traceback (most recent call last):
  File "x.py", line 5, in <module>
    calculate()
  File "x.py", line 4, in calculate
    return divide(1, 0)
           ~~~~~~^^^^^^
  File "x.py", line 2, in divide
    return a / b
           ~~^~~
ZeroDivisionError: division by zero

Read bottom-up: the exception type and message tell you what; the frames tell you where. Python 3.11+ adds fine-grained location indicators (the carets pointing at a / b). Use them.


10. Cheat sheet of dis, gc, sys

python
import dis, gc, sys
dis.dis(func)                  # show bytecode
sys.getsizeof(obj)             # bytes (shallow)
sys.getrefcount(obj)           # refcount
sys.setrecursionlimit(10000)
gc.get_count()                 # GC gen counts
gc.collect()

Sprinkle these into your IPython sessions when curious.


Hands-on lab (1.5 hours)

  1. Install IPython; play with ?, ??, %timeit.
  2. Disassemble three small functions; note opcodes for for, if, +.
  3. Compare is and == for small and large ints, for strings, for lists.
  4. Write a function that creates a reference cycle; verify it isn't freed until gc.collect().
  5. Time time.sleep running in a thread pool vs sequentially (I/O-bound, GIL releases) — show speedup. Then time a CPU-bound function (e.g., sum(i*i for i in range(10**6))) and show no speedup.
  6. Bonus: if you can build CPython 3.13 with --disable-gil, run the CPU-bound benchmark on threads; observe scaling.

Common pitfalls

  1. Mutating a default argument: def f(x=[]): x.append(1); return x. The [] is created once at definition. Use None sentinel.
  2. Using is for value comparison ("works for small ints, breaks for large").
  3. Assuming threads scale CPU work in standard CPython.
  4. Editing .pyc files (they regenerate; pointless).
  5. Forgetting __pycache__/ in .gitignore.

Self-check

  1. What does CPython do between python script.py and "hello world"?
  2. State the LEGB rule.
  3. Why doesn't threading speed up [i*i for i in range(10**8)]?
  4. Difference between is and ==.
  5. Name three Python implementations.

References

  • Fluent Python, 2nd ed., Luciano Ramalho — Chapters 1, 6, 7, 19.
  • Python Cookbook, 3rd ed., Beazley & Jones.
  • Inside the Python Virtual Machine, Obi Ike-Nwosu (free online).
  • PEP 703 — Making the GIL Optional.
  • CPython source code: https://github.com/python/cpython, particularly Python/ceval.c.
  • Beazley, "Understanding the Python GIL" (PyCon talk, evergreen).

Sign in to save your progress and earn badges.