Python: Interpreted or Compiled?
A programming language exists as an abstract specification of syntax and semantics; an implementation is a concrete program that realizes that specification. Python’s reference implementation, known as CPython, is written in C and follows a two-stage pipeline that combines compilation and interpretation. When you invoke:
python myscript.py
CPython first performs lexical analysis and parsing of the source text, producing an abstract syntax tree (AST) that captures the structure of control flow, expressions and declarations. That AST is then translated into bytecode, a compact, platform-neutral instruction set stored in memory and cached on disk as .pyc files.
Bytecode differs fundamentally from machine code. Machine code consists of processor-specific opcodes (for example x86 or ARM) executed directly by hardware. Bytecode is the instruction language of a virtual machine and requires a dispatch engine to turn each opcode into effects on memory, registers and objects. In CPython the dispatch engine is the interpreter loop—a routine written in C that reads each bytecode instruction in turn and performs the corresponding operation on Python’s object model, whether loading a constant, invoking a method or managing the call stack.
Because CPython compiles to bytecode and then interprets that bytecode, it occupies the midpoint between a naïve line-by-line interpreter and a classic ahead-of-time compiler producing a standalone native binary. To deepen one’s understanding of that midpoint, consider two contrasting compilation strategies: ahead-of-time (AOT) compilation translates source into machine code before execution, trading dynamic flexibility for raw speed; just-in-time (JIT) compilation monitors execution at runtime, identifies frequently executed paths and emits optimized machine code on the fly.
Alternative Python runtimes illustrate that spectrum. PyPy reuses the same bytecode format but embeds a JIT compiler that observes hot loops and functions at runtime, yielding performance often two to four times faster than CPython in long-running programs. Cython and Nuitka perform AOT translation of Python or Python-like code into C or C++, then invoke a native compiler to produce extension modules or executables that start faster and run compute-bound tasks more efficiently. MyPyC compiles statically typed Python modules into C extensions, achieving a further blend of Python’s expressiveness with C’s performance. Other implementations such as Jython and IronPython target the Java and .NET virtual machines respectively, relying on those hosts’ JIT facilities and garbage collectors.
Each approach represents trade‐offs. CPython’s bytecode interpreter maximizes portability, introspection and dynamic features (reflection, metaprogramming) but incurs dispatch overhead. AOT compilation yields faster startup and raw computation speed but limits runtime modification. JIT blends the two, offering near‐native performance for hotspots while preserving most of Python’s dynamism.
In standard practice, then, Python executes by first compiling to bytecode and then interpreting that bytecode. Alternative implementations extend or replace one of those stages, placing Python on the continuum between pure interpretation and full ahead‐of‐time compilation.