Python · Under the Hood
How Does Python Programming Actually Work? Explained Simply
Quick summary — how does Python programming actually work
How does Python programming actually work? Your source code is compiled into bytecode, which the Python virtual machine executes one instruction at a time. Variables are names pointing to objects in memory, reference counting and a garbage collector free unused objects, and imports find, run and cache modules so they load once.
In this guide you will learn:
- Compile and run — source, bytecode and the virtual machine.
- Objects and memory — names, references and garbage collection.
- Imports and modules — how Python finds and loads code.
- Exploration templates — experiments to see it yourself.
- Tips — why this knowledge helps you write better code.
SECTION 01Compile and run: from source code to bytecode
Python is often called interpreted, but the standard implementation, CPython, first compiles your source code into bytecode — a compact set of instructions for the Python virtual machine (PVM). The PVM then reads and executes those instructions in a loop. When you import a module, CPython saves its bytecode in a __pycache__ folder as a .pyc file, so later imports can skip recompiling if the source has not changed.
| Stage | What happens | Where you see it | Why it matters |
|---|---|---|---|
| Source code | Text you write in .py files | Your editor | Readable instructions for humans |
| Compilation | Source is parsed and turned into bytecode | SyntaxError appears before anything runs | Errors in syntax are caught early |
| Bytecode | Low-level instructions for the PVM | dis module, .pyc files | Explains performance details |
| Execution | PVM runs bytecode step by step | Program output and tracebacks | Runtime errors appear here |
import dis
def add_tax(price, rate=0.18):
return price * (1 + rate)
dis.dis(add_tax)
# Output shows instructions such as LOAD_FAST, BINARY_OP
# and RETURN_VALUE (exact names vary by Python version)
# Compile without running: syntax errors appear here
src = "total = 10 +"
try:
compile(src, "<demo>", "exec")
except SyntaxError as e:
print("Caught before running:", e.msg)
# Byte-compile a file and look for the cache
# python -m py_compile mymodule.py
# ls __pycache__/ -> mymodule.cpython-312.pyc
SECTION 02Objects and memory: names point to objects
In Python, everything is an object — numbers, strings, lists, functions, even classes. A variable is a name that refers to an object; assignment does not copy the object, it binds another name to it. This is why changing a list through one name shows up through another. CPython frees memory mainly through reference counting: when no names refer to an object, it can be released. A separate garbage collector handles reference cycles.
| Concept | Plain meaning | Quick test | Common surprise |
|---|---|---|---|
| Name binding | A variable is a label on an object | b = a; b is a → True | Assignment does not copy |
| Mutability | Some objects can change in place | Lists change, tuples do not | Changes visible through all names |
| Reference counting | Count of references to an object | sys.getrefcount(obj) | Count includes the function argument |
| Garbage collector | Cleans up reference cycles | gc.collect() | Cycles are not freed by counting alone |
a = [1, 2, 3]
b = a # same list, two names
b.append(4)
print(a) # [1, 2, 3, 4]
print(a is b) # True: same object
c = a.copy() # a new list object
c.append(5)
print(a, c) # [1, 2, 3, 4] [1, 2, 3, 4, 5]
print(id(a) == id(c)) # False
import sys, gc
data = [0] * 1000
print(sys.getrefcount(data)) # usually 2: 'data' + the call argument
alias = data
print(sys.getrefcount(data)) # one higher
del alias
print(sys.getrefcount(data))
# Reference cycle: two lists pointing at each other
x, y = [], []
x.append(y); y.append(x)
del x, y
print("collected objects:", gc.collect())
SECTION 03Imports and modules: how Python finds code
When you write import json, Python checks whether the module is already loaded in sys.modules. If not, it searches the folders listed in sys.path — your script's folder, installed packages and the standard library — finds the module, compiles it if needed and runs its top-level code once. The result is cached, so later imports are fast. This also explains why a local file named json.py can accidentally shadow the real module.
| Step | What Python does | How to inspect | Common problem |
|---|---|---|---|
| Check cache | Look in sys.modules | "json" in sys.modules | None — it is a speed-up |
| Search path | Look through sys.path in order | print(sys.path) | Local file shadows a library |
| Load and run | Compile and run module top-level code once | print in module runs on first import | Side effects at import time |
| Bind name | Make the module available in your namespace | import json as j | Circular imports |
import sys
print(sys.path[:3]) # where Python looks first
print("json" in sys.modules) # probably False before import
import json
print("json" in sys.modules) # True now
print(json.__file__) # where it was loaded from
# greet.py
def hello(name):
return f"Hello, {name}"
print("greet.py top-level code runs, __name__ =", __name__)
if __name__ == "__main__":
print(hello("from the command line"))
# python greet.py -> __name__ is "__main__", both prints run
# import greet -> __name__ is "greet", only the first print runs
SECTION 04Experiments to see Python at work
Try these small experiments to make the ideas concrete. Each takes a few minutes.
1. Run dis.dis on a function with a loop; find the jump
2. Create a module, import it twice; see top-level code
run only once
3. Make a list, give it two names, change it through one
4. Create a file named random.py; import random and
see what breaks (then delete the file!)
5. Use timeit to compare 'x in list' vs 'x in set'
import timeit
setup = "items = list(range(100_000)); s = set(items)"
print("list:", timeit.timeit("99_999 in items", setup=setup, number=1_000))
print("set :", timeit.timeit("99_999 in s", setup=setup, number=1_000))
# The set lookup is typically far faster.
Experiment:
What I expected:
What happened:
Why (in my own words):
How this changes the code I write:
SECTION 05Why understanding Python internals helps you
You do not need to read CPython's source, but a mental model helps daily:
- Avoid shared-list bugs — knowing names point to objects prevents accidental changes.
- Choose the right data structure — understanding lookups explains why sets and dicts are fast.
- Debug import errors quickly — sys.path and shadowing explain most of them.
- Keep modules side-effect free — import-time code runs once and can surprise you.
- Answer interview questions confidently — "is Python compiled or interpreted?" has a clear answer.
SECTION 06Interview Q&A — how Python works
Q1Is Python compiled or interpreted?
Both, in a sense. CPython compiles source code to bytecode, then the Python virtual machine interprets that bytecode. Other implementations, such as PyPy, use just-in-time compilation.
Q2What is the difference between "is" and "=="?
"is" checks whether two names refer to the same object in memory; "==" checks whether their values are equal. Two different lists can be equal but not the same object.
Q3How does Python manage memory?
CPython uses reference counting to free objects when no references remain, plus a cyclic garbage collector to clean up groups of objects that refer to each other.
Q4What happens when you import a module?
Python checks sys.modules, searches sys.path if needed, compiles and runs the module's top-level code once, caches it and binds the name in your namespace.
Q5What is a .pyc file?
A cached bytecode file stored in __pycache__ so Python can skip recompiling a module whose source has not changed.
SECTION 07Test yourself — how Python works quiz
Five questions. No sign-up.
0 / 5Pick an answer to see why it is right or wrong.
SECTION 08Frequently asked questions
How does Python run code?
CPython compiles your source code into bytecode and the Python virtual machine executes it instruction by instruction, managing objects in memory as it goes.
Why is Python slower than C?
Python does more work at runtime — dynamic typing, object management and bytecode interpretation — which adds overhead. Libraries written in C, such as NumPy, help for heavy computation.
What is the Python virtual machine?
It is the part of the interpreter that executes bytecode, reading instructions one at a time and performing operations on Python objects.
Do I need to know Python internals to get a job?
Not deeply, but understanding names and objects, mutability, imports and basic memory management helps you avoid bugs and answer common interview questions.
What is CPython?
CPython is the standard, most widely used implementation of Python, written in C. When people say "Python", they usually mean CPython.
SECTION 09Related reads
Classroom & online · Noida
Understand Python, not just use it
Our Python Training Course covers Python fundamentals with clear explanations of how the language works, then builds up to real projects with mentor support.
Talk to a counsellor for current fees- Fundamentals explained clearly
- Hands-on experiments and projects
- Interview-ready concepts
- Weekday & weekend batches