Text to tree
A program starts as source code: plain text a person typed. The CPU cannot run text. A compiler or an interpreter turns it into something that can run, and both begin with the same two jobs.
First the lexer (also called a tokenizer) reads the characters and groups them into tokens, the words of the language. return len(alist) becomes five tokens: three names (return is a keyword, a reserved name) and two brackets.
Next the parser checks the tokens against the grammar, the rules of the language, and builds a syntax tree. The tree holds meaning: return the result of calling len with alist. A missing bracket stops everything here with a SyntaxError.
Compile ahead of time
A compiler such as gcc or clang works before the program runs. For the C function int add(int a, int b) { return a + b; }, clang writes machine code, two arm64 instructions the CPU runs directly: add and ret.
The finished file runs straight on the CPU, with no translator in between, so it starts fast and runs fast. The cost: arm64 code only runs on arm64 chips. For an Intel or AMD PC (x86-64) you compile again.
Bytecode and a VM
CPython, the standard Python, compiles the tree to bytecode instead: simple instructions for a made-up machine. The dis module shows them. LOAD_GLOBAL finds len, LOAD_FAST gets alist, CALL calls len and RETURN_VALUE hands back the answer.
Bytecode is not machine code, so a program runs it: the virtual machine (VM), an interpreter. It is a loop: fetch one instruction, do it, go to the next. Here it pushes len and alist onto a stack, calls len and returns the result.
Each bytecode instruction costs the VM several machine instructions of its own, so this runs slower than compiled code. The gain: no build step, so it starts at once, and the same bytecode runs on any computer with that Python version.
JIT and trade-offs
A just-in-time (JIT) compiler mixes both. JavaScript's V8 engine and PyPy start by interpreting, notice which code runs again and again (hot code), and compile just that code to machine code while the program runs.
No road wins everything. Compiling ahead costs a build step and ties the file to one kind of chip, but runs fast from the start. Bytecode starts at once and travels, but runs slower. A JIT warms up first, then runs hot code fast.
What happens
;; step 1/10 · source source code plain text in a file 1 def myfunc(alist): 2 return len(alist) the CPU only runs machine code (numbers), so another program must translate the text: 1 lexing text -> tokens 2 parsing tokens -> syntax tree 3 then machine code, bytecode, or a JIT
The bytecode is CPython 3.13's, as in the dis documentation. Bytecode changes between versions: CPython 3.14 prints LOAD_FAST_BORROW instead of LOAD_FAST. (len + NULL) means LOAD_GLOBAL also pushes a NULL that CALL uses. The machine code is Apple clang 21 -O2 output for int add(int a, int b) { return a + b; } on arm64. For x86-64 the same compiler writes lea eax, [rdi + rsi] and ret. The C and Python examples are different functions.