Part I — Foundations: the Python way
Part I covers the language you need to read any Python program. Chapters 3–9 move quickly if you know another language. Chapter 2 (names and objects) is where Python differs from Go, C, and Rust, and it explains more production bugs than any other chapter. Read it slowly and run every snippet. Part I ends with a checkpoint and two projects.
1. What Python is: CPython, bytecode, and the REPL
1.1 The language and the interpreter
"Python" is a language specification. CPython is the reference
implementation, written in C, and it is what you get from python.org, uv,
Homebrew, and Docker's python images. Other implementations exist (PyPy
with a JIT, MicroPython for microcontrollers), but the data and AI
ecosystem assumes CPython, because NumPy and PyTorch are CPython
extensions. In this guide, "Python" means CPython.
What happens when you run python app.py:
app.py ──▶ tokenizer ──▶ parser ──▶ AST ──▶ compiler ──▶ bytecode ──▶ eval loop (C)
text tree code objects one bytecode
(cached in instruction
__pycache__/ at a time
*.pyc)
There is a compiler, but it compiles to bytecode for a virtual machine,
not to machine code. The VM is a loop in C (ceval.c) that executes one
instruction at a time. Chapter 25 opens that loop. For now, two facts
matter:
- Nothing is checked until the line runs. A typo in a function name
inside an
ifbranch that never executes is not an error. Linters and type checkers (Ch 18) exist to catch what the compiler does not. - Every operation is dynamic.
a + blooks up the type ofaat run time and calls its__add__method. That flexibility is the source of both Python's expressiveness and its slowness.
Since 3.11, CPython has a specializing interpreter that rewrites hot instructions for the types it actually sees (Ch 25), which made it 1.25× to 2× faster than 3.10. 3.13 added an experimental JIT and an experimental free-threaded build without the GIL; in 3.14 the free-threaded build is officially supported (Ch 27).
1.2 The REPL: your most important tool
python3.13 # or: uv run python
>>> 2 ** 100
1267650600228229401496703205376
>>> name = "Ada"
>>> f"hello, {name}"
'hello, Ada'
>>> help(str.split) # documentation for any object
>>> dir("x") # every attribute a string has
>>> type(3.0), type([]), type(len)
(<class 'float'>, <class 'list'>, <class 'builtin_function_or_method'>)
Since 3.13 the REPL has colour, multi-line editing, and paste mode (F3). Use it constantly: whenever you are unsure what an expression returns, ask the interpreter instead of guessing. In data work, Jupyter notebooks (Ch 62) are the same idea with saved outputs and plots.
1.3 Your first program
# hello.py
import sys
def main() -> int:
who = sys.argv[1] if len(sys.argv) > 1 else "world"
print(f"Hello, {who}!")
return 0
if __name__ == "__main__":
raise SystemExit(main())
$ python3 hello.py Linux
Hello, Linux!
$ echo $?
0
Every piece of this shape matters:
sys.argvis the argument list, as in C'sargv(OS Ch 3).if __name__ == "__main__":runsmain()only when the file is executed, not when another module imports it (Ch 9).raise SystemExit(main())turnsmain's return value into the process exit code, which shells,systemd, and Kubernetes read.-> intis a type hint. Python ignores it at run time; tools read it.
Indentation is syntax. Blocks are defined by indentation (4 spaces, by
convention and by ruff format), not braces. Mixing tabs and spaces is an
error.