Python for AI Engineers

◆ Chapter 01

Why Python runs the AI world (and how to think in it as a JS dev)

Why Python won the AI race, the honest trade-offs vs Node, and the mental-model shift that trips JS devs up in week one.

~1,817 words · chapter 1 of 15

You already know how to program. Loops, closures, async, promises, modules — those concepts transfer directly. What you're really learning is a second dialect and the culture around it, because that culture is where every LLM SDK, every agent framework, and every model you'll ever load lives. This chapter is about why that's true and how to retune your instincts from JavaScript to Python.

Why Python won the AI/ML race

It wasn't speed. Python is, on paper, one of the slower mainstream languages. It won for a stack of reasons that compound:

1. "Slow language, fast libraries." This is the single most important thing to internalise. Python the interpreter is slow at raw loops, but almost no serious numerical work runs in Python. Libraries like NumPy, PyTorch, and pandas are thin Python skins over C, C++, CUDA, and increasingly Rust. When you write a @ b to multiply two matrices, Python spends microseconds dispatching and the actual multiply happens in hand-optimised, SIMD-vectorised, GPU-accelerated native code. Python is the remote control; the TV is C.

import numpy as np

a = np.random.rand(1000, 1000)
b = np.random.rand(1000, 1000)
c = a @ b          # one million-plus multiply-adds, executed in native BLAS
print(c.shape)     # => (1000, 1000)

JS → Python: This is the opposite of the Node mindset, where you push everything into JS because crossing into native (N-API) is painful. In Python, crossing into native is the default and the whole ecosystem is built for it. "It's just a wrapper around C" is a compliment, not a criticism.

2. A 20-year head start in scientific computing. NumPy (2006), SciPy, matplotlib, then pandas and scikit-learn built a gravitational well. When deep learning arrived, TensorFlow and PyTorch were written to plug into that existing NumPy-shaped world. Researchers already lived there. Papers shipped with Python. The moat kept deepening.

3. Readability as a research tool. ML research is a write-once-read-many activity — you're translating a paper's maths into code that someone else will scrutinise. Python's clean, low-ceremony syntax reads almost like pseudocode, which matters enormously when the idea is the hard part, not the plumbing.

4. Glue. Python is a superb orchestration language. It shells out, talks to databases, calls REST APIs, spawns subprocesses, and binds C libraries — so it naturally became the layer that wires models to data to serving.

5. The LLM/GenAI world is Python-first, full stop. The official openai, anthropic, and google-genai SDKs are Python (with JS as a first-class second). But the frameworks — LangChain, LlamaIndex, Hugging Face transformers, vLLM, the agent orchestration libraries — are Python-native, and many have no equivalent elsewhere. If you want to read the code the field is actually written in, it's Python.

Reading & judging: When you see heavy for loops iterating element-by-element over big arrays in supposedly performance-sensitive Python, that's a red flag — the author is fighting the language instead of vectorising into NumPy/PyTorch. Solid numerical Python pushes work down into library calls; sloppy numerical Python does arithmetic in interpreted loops.

The honest trade-offs vs Node

Don't let anyone sell you Python as strictly better. Know the costs:

Dynamically typed, like JS — but with less tooling reflex. Python is duck-typed. Type hints exist (Chapter 2 covers the checkers) and are widely used in good GenAI codebases, but they're optional and not enforced at runtime by default. A function annotated -> str can still return None and Python won't stop it.

The GIL — understand it precisely. CPython has a Global Interpreter Lock: only one thread executes Python bytecode at a time. This means Python threads do not give you parallel CPU on pure-Python code. But two crucial nuances:

  • I/O releases the GIL. Waiting on network, disk, or an LLM API call frees other threads to run. So threads (and async) are fine and effective for I/O-bound work — which is most of what agent code does.
  • Native library calls release the GIL. NumPy/PyTorch drop the lock during their C computation, so heavy math does run in parallel across cores.
  • For genuine parallel Python CPU work you use multiprocessing (separate processes, separate interpreters). And note: Python 3.13 shipped an experimental free-threaded / "no-GIL" build, so this constraint is slowly loosening.

JS → Python: Node's model is one thread + an event loop and you never think about a GIL. Python gives you real threads but a lock that serialises Python bytecode. The practical upshot for agent code is similar to Node: lean on async/await for concurrent API calls; reach for processes only when you're CPU-bound in pure Python.

Slower raw loops — which, per the section above, rarely matters because the hot paths are native.

The mental-model shift

Here's what will actually trip your fingers up in week one.

Indentation is the syntax. There are no braces. A block is defined by its indentation level (4 spaces, by convention). This isn't cosmetic — get it wrong and the program's meaning changes or it won't parse.

def classify(score: float) -> str:
    if score > 0.8:
        return "confident"      # this line belongs to the if
    return "uncertain"          # this line belongs to the function

No semicolons. One statement per line; the newline ends it. (You can use ; but idiomatic Python never does.)

snake_case, not camelCase. Functions and variables are snake_case, classes are PascalCase, constants UPPER_CASE. This is near-universal — mixing camelCase into Python instantly reads as "written by someone who didn't learn the culture."

Batteries included. Python's standard library is vast and you're expected to use it: json, pathlib, datetime, itertools, collections, dataclasses, os, subprocess — all built in, no install. Compared to Node's minimalist stdlib (where you npm install for everything), Python ships a workshop.

The Zen of Python. Type import this in a REPL and you get a poem of design values. The load-bearing line: "There should be one — and preferably only one — obvious way to do it." Python culture prizes a single idiomatic path over JS's "there are nine ways to iterate an array." When you read Python and something looks weirdly uniform across projects, that's the Zen at work — and it makes code judgeable, because deviations stand out.

Truthiness differs — a preview you'll get burned by. Empty containers are falsy in Python. [], {}, (), "", 0, and None are all falsy; a non-empty list is truthy.

items = []
if not items:
    print("empty!")        # => empty!  (in JS, [] is TRUTHY — the opposite!)

JS → Python: In JavaScript if ([]) runs the block (empty arrays are truthy). In Python if [] does not. This flip is a classic bug when JS devs write Python: if not results: correctly means "no results" in Python, whereas the JS habit of checking .length has no analogue. Also, Python has no undefined — there is only None (one billion-dollar mistake instead of two).

Reading & judging: A quick culture-check when skimming an unfamiliar Python file: consistent 4-space indentation, snake_case names, stdlib used instead of hand-rolled utilities, and if not seq: style truthiness checks all signal an author fluent in the language. camelCase everywhere, if len(x) == 0:, and reinvented wheels signal a translator from another language who may not know the idioms — read their logic more carefully.

How Python actually runs

CPython is the interpreter. "Python" almost always means CPython, the reference implementation written in C. It compiles your .py source to bytecode (those __pycache__/*.pyc files) and executes it on a virtual machine. There's no separate build step you invoke; it happens on import.

python script.py runs a file top to bottom. (Use python3 on macOS/Linux if a legacy python points at Python 2 — modern installs alias python to 3.)

python script.py          # run a file
python -c "print(2**10)"  # run a one-liner  => 1024

The REPL — type python with no arguments and you get an interactive prompt where each line is evaluated immediately. It's your scratchpad for "what does this function return?" — invaluable when reading unfamiliar code.

>>> from anthropic import Anthropic
>>> client = Anthropic()          # explore an SDK live
>>> type(client.messages)
<class 'anthropic.resources.messages.Messages'>

Jupyter / Colab notebooks — where AI practitioners actually live. A notebook is a file (.ipynb, really JSON under the hood) made of cells. Cells are either Markdown (prose, LaTeX maths, images) or code. You run a code cell and its output — text, tables, inline plots — renders right beneath it. You build an analysis incrementally: load a model in one cell, prompt it in the next, chart the results below. Google Colab is a hosted Jupyter with free GPUs, which is why so many model demos are Colab links.

Why practitioners prefer them: ML work is exploratory and expensive. You load a 5 GB model once into memory, then iterate on prompts and plots for an hour without reloading. The persistent kernel (the live Python process behind the notebook) makes that fast and tactile in a way a script's run-exit cycle never is.

JS → Python: The nearest JS analogue is a browser devtools console or an Observable notebook — but far more central to daily work. In web dev, notebooks are a curiosity; in AI, they're the primary workbench. You'll read a lot of .ipynb files.

The reading gotcha that will bite you: a notebook's cells share one mutable namespace and can be run in any order. The number in [12] next to a cell is its execution order, not its position on the page. Someone can define model in cell 8, run cell 3 which uses it, delete cell 8, and the notebook still "works" in their live session — but is broken for you when you run it top to bottom. Hidden state is the notebook's original sin.

Reading & judging: When reviewing a notebook, check the execution counters (In [n]) run in order down the page with no gaps — out-of-order or missing numbers mean the results may depend on cells that were edited or deleted, so you cannot trust the outputs as reproducible. A trustworthy notebook produces the same results under Restart Kernel → Run All. Ask "does this survive a clean restart?" before you believe any number in it. Production code, by contrast, belongs in .py modules — a repo whose entire logic lives in notebooks is a maturity red flag.

Try it: Install Python 3.12+ (python3 --version to check), run python, and type import this to read the Zen, then import antigravity for the classic Easter egg. Then run python -c "print([] == False, not [])" and sit with the output False True — the empty list isn't equal to False, but it is falsy. That distinction is pure Python.