Python deep enough to be dangerous
Python without the wandering bootcamp pile-on. Functions, classes, comprehensions, generators, async, types, packaging — the core that compounds, with the parts you can defer.
Prerequisites
00.2
Stack
Python 3.12 (via uv)ruffpytesttype hints + mypy or pyright
By the end of this module
- Read and write idiomatic modern Python with type hints, dataclasses, and decent error handling.
- Set up a Python project the modern way (uv, src layout, ruff, pytest) without copying ten Stack Overflow answers.
- Write generators and async functions, and know which problem each is for.
- Ship a small typed CLI tool that does something useful, in roughly an evening.
Most “learn Python” resources do one of two things wrong. Either they spend three weeks on print() and if/else before reaching anything interesting, or they dump every feature of the language on you in a single 600-page brick and call it complete. This module is neither. It is the working subset of Python that pays off the most, with the parts you can safely defer marked clearly.
The goal of this module is not to make you a “Python expert.” It is to make you dangerous: someone who can build a real CLI tool, a real script, a real backend, in modern, typed, well-structured Python, without flailing. That is a different goal from cramming the entire language. Most working Python engineers use about 40% of the language, daily, fluently. We will get you to that 40%.
One opinion before we start: stop using pip directly. Stop using pyenv. Stop using poetry, conda, pipenv, virtualenv, hatch. The state of the art in 2026 is uv by Astral. It replaces all of them, it is roughly 10x faster, and the ergonomics are dramatically better. If you’ve never used those other tools, lucky you — skip the trauma and start at uv.
Set up
# uv installs Python itself, manages venvs, locks dependencies — all in one binary.
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install Python 3.12 (and pin it for this project)
uv python install 3.12
# New project, src layout, with type-checking ready
mkdir python-deep && cd python-deep
uv init --lib
uv python pin 3.12
uv add --dev ruff pytest mypy
# Project structure should look like:
tree
# python-deep
# ├── pyproject.toml
# ├── src/python_deep/__init__.py
# └── tests/
If uv init --lib doesn’t lay out src/ exactly that way on your version, do it by hand — src/your_package/ is the right layout for anything past a 50-line script.
Read these first
Four sources, in this order, then stop.
- Real Python — Python 3 cheat sheet. link · 30 min · just for the syntax surface area.
- Trey Hunner — Python comprehensions. post · 25 min · one of the language’s distinctive features, taught well here.
- Hynek Schlawack — Python application packaging in 2024. post · 30 min · the modern packaging story, no clutter.
- Brett Slatkin — Effective Python (2nd ed). book · the single best “next book” once you have the basics; read selectively, not cover-to-cover.
Skip “Learn Python in 10 Hours” YouTube series. They will spend half the runtime on for loops you already understand.
What to actually learn
The list, in roughly the order to attack it. Each item is one short coding session.
| # | Concept | Why it matters | Defer until later |
|---|---|---|---|
| 1 | Functions, args, kwargs, *args, **kwargs | The unit of reuse | — |
| 2 | f-strings | Daily-use formatting | — |
| 3 | List/dict/set comprehensions | Idiomatic Python everywhere | — |
| 4 | Classes + dunder methods (__init__, __repr__, __eq__) | Modeling things | metaclasses, descriptors |
| 5 | Dataclasses | Replaces 80% of what you’d use a class for | attrs, pydantic-as-class |
| 6 | Type hints (basic) | Modern Python, real codebases use them | full PEP 695 generics |
| 7 | Exceptions and try/except/finally | Reading errors, writing safe code | exception chaining nuance |
| 8 | Context managers (with) | File/db/lock cleanup | writing your own |
| 9 | Generators (yield) | Streaming data, lazy iteration | full coroutine semantics |
| 10 | Decorators | Reading framework code | writing your own complex ones |
| 11 | pathlib over os.path | Modern way, much cleaner | — |
| 12 | async/await (basics) | Real network code, modern web frameworks | full asyncio internals |
What’s not on this list, and why:
- Metaclasses, descriptors, the data model in full. You won’t write one for years. When you need it, that day, you’ll learn it in an hour.
- The full standard library. Learn
os,sys,pathlib,json,subprocess,collections,itertools,dataclasses,typing. The other 200 modules — when you need them. - Threading and multiprocessing. Real concurrency in Python is async or “use a different language for this part.” Threading is rarely the right tool. Defer.
- OOP design patterns. Inheritance hierarchies and visitor patterns are mostly a Java import. Modern Python prefers protocols and dataclasses.
Idiomatic modern Python
Here’s roughly what tasteful Python looks like in 2026. If your code drifts far from this, something’s off.
from dataclasses import dataclass
from pathlib import Path
import json
@dataclass(frozen=True)
class Repo:
owner: str
name: str
stars: int
@property
def full_name(self) -> str:
return f"{self.owner}/{self.name}"
def load_repos(path: Path) -> list[Repo]:
"""Load repos from a JSON file. Raises FileNotFoundError if missing."""
raw = json.loads(path.read_text())
return [Repo(**r) for r in raw]
def top_n(repos: list[Repo], n: int = 5) -> list[Repo]:
return sorted(repos, key=lambda r: r.stars, reverse=True)[:n]
Things to notice. Type hints on every function signature. frozen=True dataclass — immutable, so it can be hashed, compared, and not accidentally mutated. pathlib.Path.read_text() instead of open(). list[Repo] lowercase — this is the modern syntax (PEP 585), not List[Repo]. Docstring states what the function does and what it raises.
That’s the shape of every well-written Python file you’ll read in a real codebase.
Generators and async — what each is for
This is where most students get confused. Both look similar; both use yield-ish syntax in some way. They solve different problems.
Generators are for lazy iteration. When you have a sequence too big to fit in memory, or when you only want to compute as many elements as the consumer needs.
def read_lines(path: Path):
with path.open() as f:
for line in f:
yield line.rstrip()
# Consumes one line at a time, never loads the whole file.
for line in read_lines(Path("big.log")):
if "ERROR" in line:
print(line)
Async is for concurrent I/O-bound work. When you have many network or disk operations and you want them in flight together instead of one-by-one.
import asyncio
import httpx
async def fetch(url: str) -> str:
async with httpx.AsyncClient() as client:
r = await client.get(url)
return r.text
async def main():
urls = ["https://example.com", "https://example.org"]
results = await asyncio.gather(*(fetch(u) for u in urls))
print(len(results))
asyncio.run(main())
Generators are about memory. Async is about waiting. Use a generator when the data is large; use async when the work is I/O. Use neither when the work is CPU-bound — for that, you reach for a different language or multiprocessing.
Project hygiene
A real project has these four things from day one. They take ten minutes to set up and save you days over the lifetime of the repo.
# 1. Ruff for linting + formatting (replaces flake8, black, isort)
uv add --dev ruff
# 2. Pytest for tests
uv add --dev pytest
# 3. Type checking (pyright is faster, mypy is more conservative)
uv add --dev mypy
# 4. A pyproject.toml with the right settings
cat >> pyproject.toml <<'EOF'
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.mypy]
strict = true
EOF
A typical loop looks like:
uv run ruff format .
uv run ruff check . --fix
uv run mypy src/
uv run pytest
Wire those into your editor’s “format on save” and you’ll never think about them again.
The build
The build for this module is a small CLI tool that proves you can hold the whole language together at once. Here’s the spec.
Build a CLI called
gh-snapshotthat takes a list of GitHub usernames, fetches their five most-starred public repos, and writes the result as a JSON file. Usehttpxfor the network. Useargparsefor the CLI. Use dataclasses for the model. Type-hint everything. Write at least three pytest tests, mocking the HTTP calls. Runmypy --strictcleanly.
Skeleton to start from:
# src/gh_snapshot/cli.py
import argparse
import asyncio
import json
from dataclasses import asdict, dataclass
from pathlib import Path
import httpx
@dataclass(frozen=True)
class Repo:
name: str
stars: int
url: str
async def top_repos(client: httpx.AsyncClient, user: str, n: int = 5) -> list[Repo]:
r = await client.get(f"https://api.github.com/users/{user}/repos?per_page=100")
r.raise_for_status()
raw = sorted(r.json(), key=lambda x: x["stargazers_count"], reverse=True)[:n]
return [Repo(name=x["name"], stars=x["stargazers_count"], url=x["html_url"]) for x in raw]
async def main_async(users: list[str], out: Path) -> None:
async with httpx.AsyncClient() as client:
results = await asyncio.gather(*(top_repos(client, u) for u in users))
payload = {u: [asdict(r) for r in repos] for u, repos in zip(users, results)}
out.write_text(json.dumps(payload, indent=2))
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("users", nargs="+")
p.add_argument("--out", type=Path, default=Path("snapshot.json"))
args = p.parse_args()
asyncio.run(main_async(args.users, args.out))
if __name__ == "__main__":
main()
Run it:
uv run python -m gh_snapshot.cli torvalds simonw karpathy --out snap.json
cat snap.json | head
Now write the tests. Then add --limit. Then add --token for auth. Each addition is one more thing you’ve practiced.
Going deeper
When you have specific questions, in this order:
- Brett Slatkin — Effective Python. book · the single best second-pass book.
- David Beazley — Python Concurrency from the Ground Up. video · 60 min · the talk that finally makes async click.
- Hynek — Stop Writing Classes. video · 30 min · the antidote to overengineered Python.
- CPython source — Lib/asyncio. link · once you’re comfortable, read the actual implementation.
Checkpoints
If any one wobbles, the corresponding section above is what to reread.
- Why uv instead of poetry / pyenv / pip+venv? Name two specific advantages.
- What problem does a generator solve? What problem does async/await solve? Give one example of each.
- State three things modern Python code does that “old Python” tutorials get wrong (e.g., type-hint syntax, pathlib, dataclasses).
- What’s the difference between a
dataclassand a regular class with an__init__? When would you reach for each? - Show the gh-snapshot CLI working end-to-end and
mypy --strictclean.
If all five hold and the build runs, you’ve earned 01.1. Next: 01.2 — The terminal, shell, and Unix. Half of your daily power as an engineer is in the shell, not the language.