Python from First PrinciplesPost 1 of 20 · 20 of 20 written
- How to use this guide
- 01What Python actually is
- 02Names, objects and references
- 03Built-in types and what they cost
- 04Control flow and expressions
- 05Functions in depth
- 06Iteration and generators
- 07Classes and the data model
- 08Context managers and exceptions
- 09Modules, packages, imports and environments
- 10Type hints and Pydantic
- 11The performance model
- 12Concurrency and parallelism
- 13NumPy: the array model everything else copies
- 14Data and serialization
- 15PyTorch mental model
- 16AI systems engineering in Python
- 17Tooling, debugging and testing
- 18Asking the right questions
- 19Roadmap and build-to-learn projects
- 20Glossary and resources
1. What Python actually is
Python is a language specification; CPython is the program that actually runs it. When people say “Python is slow” or “Python has a GIL”, they mean CPython, the reference implementation written in C. Other implementations exist (PyPy has a JIT compiler), but the AI ecosystem is built on CPython because of its C extension API.
What happens when you run a file
- Read the source text, decoded as UTF-8.
- Parse it into an abstract syntax tree (AST): the program’s structure as a tree of nodes.
- Compile the AST to bytecode: instructions for a virtual stack machine, cached in
__pycache__/*.pyc. - Execute: the interpreter loop runs bytecode one instruction at a time, creating and manipulating objects on the heap.
You can look at each stage yourself:
import ast, dis
print(ast.dump(ast.parse("y = x * 2 + 1"), indent=2))
def f(x):
return x * 2 + 1
dis.dis(f)The dis output looks roughly like this (opcode names vary by version):
LOAD_FAST x
LOAD_CONST 2
BINARY_OP *
LOAD_CONST 1
BINARY_OP +
RETURN_VALUEThis is why pure Python is slow. BINARY_OP * does not know the types in advance: it inspects x, finds its type’s multiplication function, and calls it, every single time. Since 3.11 CPython specializes hot instructions (an int-only fast path, for example), but the model is the same.
Dynamic, strong, duck-typed
- Dynamic: types belong to objects, not to variables. A name can point at an int, then at a string.
- Strong: Python does not silently convert unrelated types.
"3" + 4raisesTypeError, where JavaScript would give"34". - Duck typing: code cares about what an object can do, not what it is. Anything with
__len__and__getitem__behaves like a sequence, which is exactly why PyTorch’sDataLoaderaccepts your own dataset class.
Everything is an object
Integers, functions, classes, modules and even types are objects. Each has a type, an identity and a value.
import math
def g(): pass
type(3) # <class 'int'>
type(g) # <class 'function'>
type(math) # <class 'module'>
type(int) # <class 'type'>
type(type) # <class 'type'> (types are objects too)The practical consequence: you can pass functions as arguments, store classes in dicts and attach attributes to anything. A framework’s registry that maps "adamw" to an optimizer class is just a dict.
Compiled or interpreted? Both - and mostly neither
Python is compiled to bytecode, then interpreted. NumPy and PyTorch are mostly C, C++ and CUDA called from Python. In AI work, treat Python as the control plane: it decides what to compute, and compiled kernels do the computing.
Questions to ask:
- Is this line running Python bytecode in a loop, or making one call into compiled code?
- Which interpreter and version am I on? Check with
import sys; print(sys.version, sys.executable).