How Python Runs Your Code: Execution Process Explained

How Python Runs Your Code: Execution Process Explained

How Python Runs Your Code: Execution Process Explained

Ever wondered what happens behind the scenes when you run a Python file using python script.py? Python may feel simple, but under the hood, it’s doing some sophisticated work to interpret and execute your code.

⚙️ Step-by-Step: How Python Executes Code

  1. Source Code (.py file)
  2. Compilation to Bytecode (.pyc)
  3. Bytecode Execution by Python Virtual Machine (PVM)

๐Ÿ”Ž High-Level Overview

The Python execution model is built around the idea of interpreted bytecode execution, not direct binary translation. This enables platform independence and rich introspection at runtime, at the cost of some speed.

Python trades off raw performance for developer productivity, rapid prototyping, and clean syntax — and it’s ideal for fields like data science, scripting, automation, and backend services.

๐Ÿ“ Step 1: Writing the Code (.py file)

Python source files are written using plain text and saved with the .py extension. These contain your actual code, variables, classes, and logic.


def greet(name):
    return f"Hello, {name}"

print(greet("Aryan"))

⚙️ Step 2: Compilation to Bytecode (.pyc)

When you run a Python file, the Python interpreter first compiles it to an intermediate form called bytecode — a lower-level, platform-independent representation.

The bytecode is stored as a .pyc file inside the __pycache__/ folder.


__pycache__/
  greet.cpython-311.pyc

You can manually compile using:


python -m compileall script.py

๐Ÿง  Step 3: Python Virtual Machine (PVM)

The Python Virtual Machine (PVM) is the runtime engine that executes bytecode line by line. This is the part of Python that interprets the code.

Each Python distribution (e.g., CPython, PyPy) has its own PVM. CPython is the default and written in C.

๐Ÿ What is CPython?

CPython is the default implementation of Python. It compiles Python to bytecode and executes it using a C-based virtual machine.

Other implementations include:

  • PyPy – Python with a JIT compiler
  • Jython – Python running on the JVM
  • IronPython – Python for the .NET CLR

๐Ÿงฐ How to Check Your Python Implementation

You can determine your current Python interpreter using:


import platform
print(platform.python_implementation())

Output:


CPython

On PyPy, it would return PyPy. This is useful for debugging and cross-platform scripting.

⏱️ Memory Management in Python

Python uses dynamic typing and automatic memory management via reference counting and a cyclic garbage collector.


a = []
b = a
del a  # 'b' still holds a reference

The object is only removed from memory when no references exist.

๐Ÿงน Python’s Garbage Collector

Python uses two memory management strategies:

  • Reference Counting: Every object tracks how many references point to it.
  • Cyclic Garbage Collector: Detects and frees up groups of objects involved in circular references.

You can interact with the GC directly:


import gc
print(gc.get_threshold())   # GC thresholds
print(gc.get_count())       # Object counts in generations

๐Ÿ” Internal Execution Flow

  • Lexer converts code into tokens
  • Parser builds AST (Abstract Syntax Tree)
  • Compiler converts AST to bytecode
  • Interpreter executes bytecode via the PVM

๐Ÿ› ️ What is Python Bytecode?

Bytecode is a set of low-level instructions executed by the PVM. It is portable and independent of the system architecture, unlike machine code.

Python’s bytecode is not optimized — it’s simple, readable, and very high-level compared to native assembly code.


LOAD_FAST, STORE_FAST, BINARY_ADD, CALL_FUNCTION, etc.

These opcodes are defined in Python’s opcode.h source file.

⚡ Bonus: Disassembling Python Bytecode

You can inspect bytecode using the built-in dis module:


import dis

def add(x, y):
    return x + y

dis.dis(add)

Sample output:


  2           0 LOAD_FAST                0 (x)
              2 LOAD_FAST                1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE

๐Ÿงช Why Understanding Execution Matters

Understanding how Python runs your code helps with:

  • Optimizing performance
  • Avoiding memory leaks
  • Writing more Pythonic code
  • Debugging complex logic

๐Ÿ”— Additional Resources

๐Ÿงญ Practical Debugging Tips

Understanding Python's internals can drastically improve your debugging skills. Try these tools:

  • sys – Inspect modules, memory, paths, recursion depth
  • dis – View bytecode of any function
  • gc – Check memory and object lifecycle
  • tracemalloc – Track memory allocations

import tracemalloc
tracemalloc.start()
# run some code
snapshot = tracemalloc.take_snapshot()
snapshot.statistics('lineno')

๐Ÿ“Œ Final Thoughts

Python may look simple on the outside, but it's powered by a deep and elegant process under the hood. From parsing and compiling to bytecode execution and memory management — every line you write goes through a fascinating journey.

Comments