This episode breaks down how Python works behind the scenes: CPython, bytecode, the interpreter loop, and the GIL. You'll also get to know alternative implementations such as PyPy, Jython, IronPython, and GraalPython, as well as Python project structure: modules, packages, the import system, and sys.path.

Most people learn Python without ever knowing what happens behind python3 main.py. Episode 2 pulls back that curtain: how the CPython interpreter works, what bytecode is, why the GIL is so talked about, and how your code is organized into modules and packages.
Understanding this architecture isn't just theory. It explains why Python is slow in certain cases, why threading doesn't always speed things up, and when to use alternative implementations. Let's dissect it layer by layer.
CPython is the reference implementation of Python, written in the C language. This is the interpreter you installed via pyenv in episode 0. CPython sets the standard because it's the most complete and the most widely used in the world.
CPython runs code in two stages: compilation to bytecode, then execution by the interpreter loop. The flow is simple:
sumber.py → bytecode → interpreter loop → hasilBytecode is an intermediate representation similar to machine code, but at a higher level. You can see the bytecode of a function using the dis module:
import dis
def tambah(a, b):
return a + b
dis.dis(tambah)The output of dis.dis(tambah) shows instructions like LOAD_FAST, BINARY_OP, and RETURN_VALUE. This proves that Python does compile code before executing it — it just caches the result in memory.
A key fact you should know: since version 3.13, CPython supports a free-threaded build that optionally removes the GIL. However, on the standard build, the GIL (Global Interpreter Lock) still exists and locks bytecode execution to only one thread at a time.
import threading
def kerja():
total = 0
for _ in range(1_000_000):
total += 1
t = threading.Thread(target=kerja)
t.start()
t.join()
print("selesai")On the standard CPython build, running kerja() on many threads does not speed up CPU computation because of the GIL. threading.Thread(target=kerja) creates a new thread, but the GIL ensures only one thread executes bytecode at a time.
The GIL makes CPython simple and safe for memory management, but it limits CPU parallelism. The common solutions are multiprocessing for heavy computation (episode 15) or using PyPy and free-threaded CPython. So don't be surprised if Python doesn't feel as fast as C for intensive calculations.
CPython isn't the only way to run Python. Several alternative implementations offer different advantages:
PyPy is attractive for intensive computation because of its JIT. Jython and IronPython are rarely used, but important when you need to interact with the Java or .NET ecosystems. For this series, we stick with CPython because it's the most compatible with modern libraries.
In Python, a module is a single .py file, while a package is a directory containing modules with an __init__.py file. This package marker is what distinguishes it from a regular directory. A simple structure looks like this:
project/
main.py
utils/
__init__.py
helper.pyThe __init__.py file can be empty or contain initialization. With this structure, you can import via from utils.helper import fungsi. The __init__.py marker makes the utils directory recognized as a package.
When you write import modul, Python looks for the module in directories registered in sys.path. The search order includes the script directory, environment variables, and site-packages. You can see the list:
import sys
for path in sys.path:
print(path)The loop for path in sys.path prints the import search locations. If a local file name collides with a library, the local file wins. That's why you should avoid naming files after standard modules like random.py or json.py.
Let's practice the concepts above in a small project:
mkdir belajar-struktur
cd belajar-struktur
touch __init__.py main.pyThe command mkdir belajar-struktur creates a directory, then touch main.py creates the main file. With this structure, you already have an empty package ready to be filled in episodes 3 and 7.
A tidy structure determines how easy maintenance is. Small, focused modules are easier to test, import, and reuse. This habit will continue into episode 7 when we discuss packaging and pyproject.toml.
Key takeaways:
In the next episode, episode 3, we'll cover syntax and program structure — built-in data types, operators, control flow, functions, and comprehensions, complete with naming and docstring best practices. This is the time to start writing lots of Python code!