Why Python Owns ML
A wiki entry to the Python topic — what makes Python the default ML language and what to learn first.
TL;DR
Every serious ML library — PyTorch, TensorFlow, JAX, scikit-learn, Hugging Face Transformers, LangChain, vLLM — exposes a Python API. The actual numerics live in C++, CUDA, Rust, or Triton, but the layer you write is Python. There is no second-place language; the gap between Python and the runner-up (Julia, briefly hopeful, has not closed it) is enormous.
That means “knowing Python” is not optional and not negotiable for an ML engineer. But “knowing Python” in this context is a specific subset: mutability and references (so you don’t accidentally share state across training runs), comprehensions and generators (so your data pipelines don’t OOM), the typing system (so your codebase survives past 5,000 lines), NumPy and Pandas (because you’re moving tensors and dataframes all day), async (because every LLM call is I/O-bound), and the toolchain around all of it.
This topic is the wiki entry for those things. It is not a beginner
programming course — if you’ve never written a for loop, start
elsewhere. It assumes you can read Python and want the working knowledge
that ML engineers have day to day.
The picture in your head
Python is a glue language with a thin layer of syntax sitting on top of
C extensions. When you call torch.matmul(a, b), the Python interpreter
spends maybe 10 microseconds dispatching the call, then hands the actual
work to a CUDA kernel that runs in 200 microseconds. The Python layer’s
job is orchestration: build the right tensors, set up the right
optimizer, schedule the right training loop. The performance-critical
work is not in Python — it’s in the C library Python is calling.
This split is the single most important fact about Python for ML. It explains why “Python is slow” is mostly irrelevant (the inner loop isn’t in Python); it explains why a clumsy Pandas one-liner can be 1000× slower than a different one-liner that does the same thing (one stays in C, one falls back to Python); and it explains why you optimize ML code by staying out of Python, not by writing faster Python.
Why Python and not something else
A few reasons, ordered by how much they actually matter.
| # | Reason | What it means in practice |
|---|---|---|
| 1 | The libraries. | Every paper releases a Python implementation. Every pretrained model on Hugging Face has a Python loader. The ecosystem moat is so deep no other language can catch up. |
| 2 | C interop is excellent. | NumPy, PyTorch, TensorFlow are all thin Python wrappers around C/C++/CUDA. You get the productivity of a scripting language with the speed of compiled code, for the bits that matter. |
| 3 | Notebooks. | Jupyter is how ML research is done. Python’s REPL-friendliness is why the notebook ecosystem exists in Python and not elsewhere. |
| 4 | Hireability. | Every ML engineer in the world reads Python. Your codebase will outlive you only if other people can read it. |
| 5 | It’s good enough for most things. | When it isn’t (hot inference paths, low-latency serving), you drop to Rust, C++, or call into a compiled extension. Python is the orchestration layer, not the answer to every problem. |
The non-reasons: “Python is elegant” (it’s fine, but Ruby is more elegant and nobody does ML in Ruby), “Python is easy” (so is JavaScript). The reason is the libraries, full stop.
What “knowing Python for ML” actually means
A working ML engineer uses these things every week. If any of them feel fuzzy, the corresponding guide in this topic is worth a slow read.
Language fundamentals you will use daily
| Concept | Why it matters in ML | Guide |
|---|---|---|
| Mutability and references | A model.parameters() list shared across optimizers; a default [] arg that accumulates across calls | PY 101 |
| Containers (list/dict/set/tuple) | Picking the wrong one is how training data preprocessing becomes O(n²) | PY 102 |
| Comprehensions | The idiomatic way to build batches, vocabularies, label maps | PY 103 |
| Iterators / generators | How DataLoader, streaming datasets, and infinite samplers actually work | PY 104 |
| Decorators | @torch.no_grad, @functools.cache, @app.post, @retry — they’re everywhere | PY 105 |
| Context managers | with torch.cuda.amp.autocast():, with model.eval_mode():, file handles, DB sessions | PY 106 |
| Type hints | Mandatory once your codebase passes ~2,000 lines | PY 107 |
| Dataclasses | The right shape for TrainingConfig, EvalResult, RunMetadata | PY 108 |
| Pydantic | The right shape for anything that crosses a network boundary or comes from YAML | PY 109 |
The numerical stack
You don’t write training loops in pure Python. You write them in NumPy and PyTorch, which means thinking in arrays and broadcasts.
| Concept | Why | Guide |
|---|---|---|
| NumPy arrays — dtype, shape, strides | How a tensor is laid out in memory; the prerequisite to vectorization | PY 201 |
| Broadcasting | The rule that makes (N, 1) - (1, M) produce (N, M) distance matrices in one line | PY 202 |
| Vectorization vs loops | A single rule: stay in C. Loops in Python over arrays are the cardinal sin. | PY 203 |
| Pandas fundamentals | The dataframe is the analyst’s lingua franca; you’ll meet it in every dataset audit | PY 204 |
| Pandas groupby | Feature engineering’s workhorse | PY 205 |
| Pandas merge / join | Where train/test contamination usually creeps in | PY 206 |
Concurrency
Most ML latency is not CPU-bound. It’s GPU kernels (handled by PyTorch), or I/O — file reads, network calls to LLM APIs, S3 fetches.
| Concept | Why | Guide |
|---|---|---|
| async / await | The way to make 1000 concurrent LLM calls without 1000 threads | PY 301 |
| asyncio for LLM batch calls | The production pattern for fan-out over an OpenAI/Anthropic SDK | PY 302 |
| The GIL | Why threading doesn’t speed up CPU-bound Python, and what to do instead | PY 303 |
Tooling
Code that doesn’t ship is code that doesn’t matter. The shipping layer in Python is its own subject.
| Concept | Why | Guide |
|---|---|---|
| Virtual envs (uv, poetry) | One project, one set of dependencies, zero global pollution | PY 401 |
| Logging | print does not survive contact with production | PY 402 |
| pytest + fixtures | Tests are how you keep a 50k-line ML codebase trustworthy | PY 403 |
| Profiling | The first move when something is slow, before any rewrite | PY 404 |
A worked example — the Python you actually write in ML
Here is a tiny but honest snippet. It loads a dataset, batches it, runs inference with an LLM client asynchronously, validates the output with Pydantic, and writes results. Every guide above shows up.
import asyncio
import logging
from dataclasses import dataclass
from pathlib import Path
import pandas as pd
from pydantic import BaseModel, Field
from openai import AsyncOpenAI
logger = logging.getLogger(__name__)
client = AsyncOpenAI()
class Classification(BaseModel):
label: str = Field(pattern=r"^(positive|negative|neutral)$")
confidence: float = Field(ge=0.0, le=1.0)
@dataclass(frozen=True, slots=True)
class JobConfig:
input_path: Path
output_path: Path
model: str = "gpt-4o-mini"
concurrency: int = 16
async def classify_one(text: str, model: str) -> Classification:
resp = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": f"Classify sentiment: {text!r}"}],
response_format={"type": "json_object"},
)
return Classification.model_validate_json(resp.choices[0].message.content)
async def run(cfg: JobConfig) -> None:
df = pd.read_csv(cfg.input_path)
sem = asyncio.Semaphore(cfg.concurrency)
async def bounded(text: str) -> Classification:
async with sem:
return await classify_one(text, cfg.model)
results = await asyncio.gather(*(bounded(t) for t in df["text"]))
out = df.assign(
label=[r.label for r in results],
confidence=[r.confidence for r in results],
)
out.to_csv(cfg.output_path, index=False)
logger.info("wrote %d rows to %s", len(out), cfg.output_path)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(run(JobConfig(Path("in.csv"), Path("out.csv"))))
Count what’s there: a dataclass for the config, a Pydantic model for validated LLM output, async fan-out with a semaphore for rate limiting, a Pandas DataFrame for I/O, structured logging, type hints throughout. That’s an honest 80 lines of ML engineering Python. None of the libraries are exotic; all of them are in this topic.
Common beginner mistakes — what trips people up
- Treating Python like Java. Long classes for small jobs.
dataclassesand module-level functions are usually the right answer for ML pipelines, not OOP hierarchies. - For-looping over NumPy or Pandas. A Python loop over a million-row Series is 100× slower than the vectorized one-liner. See PY 203.
- Mutable default arguments.
def f(x, items=[]):is the most famous Python footgun. The[]is created once and shared across every call. See PY 101. - Skipping type hints because “we’ll add them later.” You won’t. Add them as you write. mypy or pyright catches real bugs.
pip installinto the system Python. Use a venv oruv. Always.- Using
printfor logging.printhas no levels, no destination control, no structure. Logs from production are how you debug at 3am. See PY 402. - Not knowing what
isdoes.ischecks identity (same object), not equality.[1, 2] is [1, 2]isFalse.a = b; a is bisTrue. Use==for value comparison,isforNone.
Python versions and what to use
Use Python 3.11 or newer unless something pins you. 3.11 brought significant interpreter speedups (10-60% on real workloads), 3.12 improved f-strings and error messages, 3.13 made the GIL optional (experimental). The free-threaded build under PEP 703 is the most significant Python release of the decade for compute-heavy ML; production adoption will still take a couple of years.
Avoid Python 3.10 and below for new projects. Hard-avoid Python 2.
How to actually learn this
The content standard for the rest of this topic is “engineer at a
whiteboard.” Not “language tour.” Each guide picks a concept that ML
engineers use weekly, explains the mental model, shows real code, lists
the gotchas, and points at the production patterns. There are no
exhaustive language references — docs.python.org already exists.
The order to read in, if you’re new to ML Python:
- PY 101–103 — language fundamentals you’ll get wrong otherwise.
- PY 201–203 — NumPy is the lingua franca of tensors.
- PY 401, 402, 403 — set up your project, your logs, your tests.
- Then everything else as the need arises.
Resources
- Python docs — docs.python.org — the canonical reference.
- Fluent Python (2nd ed.) — Luciano Ramalho — oreilly.com — the single best book on idiomatic Python. Read cover to cover at some point.
- High Performance Python (2nd ed.) — Gorelick & Ozsvald — oreilly.com — vectorization, profiling, Numba, multiprocessing — done right.
- Real Python — realpython.com — workmanlike tutorials, better than most blogs.
- PEP Index — peps.python.org — the design specs. Read PEP 8, PEP 20, PEP 257, PEP 484, PEP 492, PEP 557 at minimum.
- Talk Python To Me podcast — talkpython.fm — interviews with maintainers; how to stay current.