The interpreter: bytecode and the GIL
CPython compiles your code to bytecode and executes it one instruction at a time, holding the Global Interpreter Lock. That explains why one line can be several steps, and why threads behave as they do.
- Identify the interpreter and version with
sys.implementation,sys.version_infoandplatform - List a function's bytecode instructions with
dis.get_instructions - Explain the GIL, why
x += 1is not atomic, and what free-threaded builds change
"Python" is a language; the program that runs it is an implementation. The standard one, CPython, is written in C. Others exist (PyPy, MicroPython), and this browser runs CPython compiled to WebAssembly (Pyodide). sys.implementation.name, sys.version_info and platform.python_implementation() say which one you have, which matters because memory behaviour and threading differ between them.
Run each example to see what it prints. Running examples doesn't count toward progress.
import platform import sys import sysconfig print(sys.implementation.name, sys.version_info[:2], platform.python_implementation()) print(sys.getswitchinterval()) print(sysconfig.get_config_var("Py_GIL_DISABLED"))
CPython first compiles each function to bytecode: small instructions for its virtual machine, such as *load a value*, *add*, *store*. The dis module shows them. dis.get_instructions(func) yields one object per instruction with an opname and an argval (the name or constant it works on). Exact instruction names change between Python versions, so read them for their meaning, not their spelling.
import dis def record_sale(totals, amount): totals["sales"] += amount steps = [ins.opname for ins in dis.get_instructions(record_sale)] print(steps) print([ins.argval for ins in dis.get_instructions(record_sale) if ins.opname.startswith("LOAD")])
The Global Interpreter Lock (GIL) is a lock inside standard CPython: a thread must hold it to execute bytecode, so only one thread runs Python bytecode at a time. Every few milliseconds (the switch interval), and whenever a thread waits for I/O, the GIL can pass to another thread, between two instructions. So:
- The GIL keeps CPython's own data (reference counts, lists, dictionaries) consistent, which is why a single
list.appendfrom two threads doesn't corrupt the list. - It does not make your read-modify-write steps atomic: another thread can run between the read and the write of
totals["sales"] += amount, and one sale is lost. That needs a lock (see Threading & Multiprocessing). - Threads still help with waiting (network, disk), because a waiting thread releases the GIL; they don't speed up pure Python computation, because only one thread computes at a time. Processes do (see Threading & Multiprocessing).
CPython 3.13 added an optional free-threaded build without the GIL (PEP 703). It can run threads truly in parallel, which makes locks around shared data even more important.
When to look at bytecode: to understand why an operation isn't atomic, to see which global names a function really touches, or to compare two ways of writing the same thing. Mistakes to avoid: relying on exact instruction names across versions; believing the GIL makes your own updates thread-safe; and expecting threads to make CPU-bound Python faster on a standard build.
sys.implementation,sys.version_infoandplatformsay which interpreter runs your code.- CPython runs bytecode;
dis.get_instructionsshows one line becoming several instructions. - The GIL lets one thread run bytecode at a time and may switch between instructions:
x += 1is not atomic. - Next module: measuring speed (timeit, cProfile) and making programs faster.