How Python Runs Your Code

Source code is compiled to bytecode and the Python Virtual Machine executes it. Knowing that explains __pycache__, import speed and most confusing error messages.

Python is not only interpreted

Python is usually described as an interpreted language, and in day to day use that is a fair description: you hand it a source file and it starts running. Underneath, however, there is a compilation step. Python compiles your source into an intermediate form called bytecode, and a program called the Python Virtual Machine executes that bytecode instruction by instruction.

The pipeline

program.py            your source code
    |  tokeniser        text  -> tokens
    v
  tokens
    |  parser           tokens -> abstract syntax tree
    v
  AST
    |  compiler         AST   -> bytecode
    v
program bytecode
    |  Python Virtual Machine
    v
  the program runs

Each stage can fail, and which stage failed tells you what kind of mistake you made.

StageWhat it checksTypical failure
TokeniserCharacters form legal tokens, indentation is consistentIndentationError, TabError
ParserTokens form legal statementsSyntaxError
CompilerStatements can be turned into instructionsSyntaxError for things such as return outside a function
Virtual machineNothing in advance; it simply runsEvery other error, at the moment the line executes
This is the key insight. A SyntaxError stops the whole file before a single line runs. A NameError or TypeError appears only when execution reaches the offending line, which is why a program can run happily for an hour and then fail on a branch nobody had exercised.

Seeing the bytecode

The standard library module dis disassembles a function so you can read the instructions the compiler produced.

import dis


def total(a, b):
    return a + b * 2


dis.dis(total)
LOAD_FAST      a
LOAD_FAST      b
LOAD_CONST     2
BINARY_OP      *
BINARY_OP      +
RETURN_VALUE

The exact output varies between Python versions, and it is not something to memorise. It is worth looking at once, because it makes two things concrete: the compiler already decided that b * 2 happens before the addition, and the machine executing your code works on a simple stack of values.

__pycache__ and .pyc files

Compiling takes time. When you import a module, Python caches the compiled bytecode in a folder named __pycache__, in a file ending in .pyc. Next time, if the source has not changed, the cached bytecode is loaded and the compile step is skipped.

project/
    main.py
    helpers.py
    __pycache__/
        helpers.cpython-312.pyc
  • Only imported modules are cached. The script you run directly is not, because it is compiled once and then the process ends.
  • The cache is keyed by the source timestamp and size, so an edit invalidates it automatically.
  • The file name records the interpreter version, so two Python versions never read each other's cache.
  • __pycache__ is generated output. It belongs in .gitignore, never in version control.

Deleting __pycache__ is always safe. Python simply rebuilds it.

Implementations

The Python you almost certainly installed is CPython, the reference implementation written in C. The language is a specification, and other implementations exist that follow it.

ImplementationWritten inNotable for
CPythonCThe reference. Everything in these notes targets it.
PyPyPythonA just in time compiler; long running numeric loops can be far faster.
JythonJavaRuns on the Java Virtual Machine.
MicroPythonCA trimmed Python for microcontrollers.

Names such as the global interpreter lock are properties of CPython, not of the Python language. That distinction matters in the concurrency notes later in this path.

Why any of this matters in practice

  • You can explain why a typo on the last line of a file prevents the first line from running.
  • You know why the second import of a large module is faster than the first.
  • You will not commit __pycache__, and you will not be alarmed by it.
  • You understand that "compiled" and "interpreted" are stages in one pipeline, not opposing categories.

Common mistakes

  • Editing a .pyc file. It is generated; edit the .py source.
  • Shipping __pycache__ or committing it, which causes noise in every diff.
  • Believing a program is correct because it started. Only the syntax was verified in advance.
  • Assuming bytecode is machine code. It is instructions for the Python virtual machine, not for your processor.

Practice

  1. Write a file whose last line is prnt("done") and whose first line is print("start"). Predict the output before running it, then explain the result.
  2. Disassemble a function containing a + b * 2 and a second containing (a + b) * 2. Describe the difference in the instruction order.
  3. Import a module twice in one session and explain why the second import does almost nothing.
  4. Explain in two sentences why a SyntaxError and a ZeroDivisionError are detected at different times.

Conclusion

Source becomes bytecode, and the virtual machine runs the bytecode. That one sentence accounts for __pycache__, for the difference between errors found before running and errors found while running, and for the fact that Python is both compiled and interpreted depending on which stage you are describing.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Introduction to Python

Python is a high level, dynamically typed, interpreted language whose whole design goal is that a program should be as easy to read as it was to write...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.