Bytecode, the GIL and the Python way

Part of the free LogicWiz Python course, Module 13: Python under the hood. The examples and practice run in the app.

Riya's senior says "Python is interpreted, it never compiles". But Riya found a __pycache__ folder full of .pyc files next to her code. Who is right? (Both, a little.)

You'll learn to

  • Explain how CPython turns source code into bytecode and runs it
  • Compare CPython, PyPy and other Pythons
  • Explain what the GIL is and when threads do or do not help
  • Know the big Python 2 vs 3 changes, PEP 8 and the Zen of Python

Learn

Source → bytecode → virtual machine

When you run a .py file, CPython first compiles it to bytecode: small, simple instructions like "load this name", "add", "return". Then the Python virtual machine (a big loop written in C) runs those instructions one by one.

For imported modules, the bytecode is saved as .pyc files in __pycache__, so the next run can skip compiling. So Python is compiled (to bytecode) and interpreted (the bytecode is run by the VM, not turned into machine code).

Look at the bytecode with `dis`

import dis

def bill(price, qty):
    return price * qty + 5

dis.dis(bill)

You will see instructions like LOAD_FAST price, BINARY_OP * and RETURN_VALUE. The exact names change between Python versions, so read them as a picture of the idea, not something to memorise.

A function carries its code object

def bill(price, qty):
    total = price * qty
    return round(total)

code = bill.__code__
print(code.co_argcount)
print(code.co_varnames)
print(code.co_names)

prog = compile("print(40 + 15)", "<till>", "exec")
exec(prog)

co_varnames lists local names (parameters first), co_names lists global and attribute names the code uses. compile() turns text into a code object; exec() runs it.

Many Pythons

"Python" is a language; there are several programs that run it:

  • CPython: the standard one, written in C. What you get from python.org. What this course uses.
  • PyPy: has a JIT (just-in-time compiler) that turns hot loops into machine code; pure-Python loops often run several times faster. Some C extensions work less well on it.
  • Pyodide: CPython compiled to WebAssembly, so it runs in a browser. It is running your code on this page right now.
  • MicroPython (small chips), Jython (Java), IronPython (.NET).

The GIL

CPython has a Global Interpreter Lock: in one process, only one thread runs Python bytecode at a time. It exists because reference counts (Lesson 13.3) are updated all the time; one lock keeps them safe and keeps single-threaded code fast.

What it means for you:

  • I/O-bound work (downloads, files, waiting for a database): threads do help, because a thread releases the GIL while it waits.
  • CPU-bound work (heavy maths in pure Python): threads don't speed it up. Use multiprocessing (each process has its own GIL) or NumPy, whose C code releases the GIL.

Python 3.13 added an experimental "free-threaded" build without the GIL; the normal build still has it.

Threads on CPU-bound work

import threading, time

def count(n):
    while n > 0:
        n -= 1

N = 2_000_000
start = time.perf_counter()
count(N); count(N)
one = time.perf_counter() - start

start = time.perf_counter()
t1 = threading.Thread(target=count, args=(N,))
t2 = threading.Thread(target=count, args=(N,))
t1.start(); t2.start(); t1.join(); t2.join()
two = time.perf_counter() - start

print(f"one thread : {one:.2f}s")
print(f"two threads: {two:.2f}s")

The browser cannot run real threads, so this shows a laptop run. Two threads took about the same time as one: the GIL lets only one of them count at a time.

Python 2 vs Python 3

Python 2 (ended in 2020)Python 3
print "hi" (a statement)print("hi") (a function)
7 / 2 gives 37 / 2 gives 3.5; 7 // 2 gives 3
str is bytes; u"..." for Unicodestr is Unicode text; bytes is separate
rawinput() reads text, input() evaluates itinput() always returns text
range builds a list, xrange is lazyrange is lazy
d.keys() returns a listd.keys() returns a live view

PEP 8 and the Zen of Python

PEP 8 is Python's style guide. The rules interviewers expect you to know:

  • 4 spaces per indent, no tabs
  • snake_case for functions and variables, CapWords for classes, UPPER_CASE for constants
  • lines up to 79 characters; spaces around = and operators, none just inside brackets
  • imports at the top, one per line

Tools like black, ruff and flake8 check and fix this for you. And for the philosophy, run import this: "Readability counts", "Explicit is better than implicit".

The Zen of Python

import this

A module runs its code only on the first import. If you press Run again in the same session, nothing prints, because Python already has this in sys.modules.

In one line

CPython compiles to bytecode and runs it in a VM; the GIL lets one thread run bytecode at a time, so threads help I/O, not CPU work.

Interview corner

What is the GIL? Why does CPython have it?

The Global Interpreter Lock is a mutex that allows only one thread to execute Python bytecode at a time in a process. It makes reference counting and the interpreter's internals thread-safe cheaply and keeps single-threaded code fast. Threads still help I/O-bound work (the GIL is released while waiting); for CPU-bound work use multiprocessing or C extensions like NumPy.

Is Python compiled or interpreted?

Both. CPython compiles source to bytecode (cached as .pyc in __pycache__ for imported modules) and then the Python virtual machine interprets that bytecode. You can see it with the dis module.

What is the difference between CPython and PyPy?

CPython is the reference implementation written in C, with an interpreter loop. PyPy is an alternative implementation with a JIT compiler that compiles hot code paths to machine code, so long-running pure-Python code is often much faster; compatibility with some C extensions is weaker.

Name some key differences between Python 2 and Python 3.

print became a function; / is true division (7 / 2 == 3.5); str is Unicode and bytes is separate; input() returns text (no raw_input); range, zip and dict.keys() return lazy objects/views instead of lists. Python 2 reached end of life in 2020.