← All 45 books Python intermediate, one move per page Get the full edition · £10
One move per page

Python intermediate, one move per page

The twenty-two moves past beginner scripts: classes, exceptions as flow, comprehensions, venvs, type hints, and a project shape your tools already understand.


Steve Hodgkiss 5 moves

You write the scripts. These are the moves.

Python intermediate, one move per page

The twenty-two moves past beginner scripts: classes, exceptions as flow, comprehensions, venvs, type hints, and a project shape your tools already understand.


Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).

Language behaviour verified against the official Python documentation: docs.python.org/3 (tutorial, library reference, HOWTOs), typing.python.org, and docs.pytest.org.

Python and the Python logo are trademarks of the Python Software Foundation. This book is an independent guide and is not affiliated with or endorsed by the PSF.

Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.

© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.

Edition 1.0 · stevehodgkiss.net

Contents

Contents


Part 1 · Objects
Classes: the blueprint4
Part 2 · Failures
try, except, else, finally5
Part 3 · Data
Comprehensions6
Part 4 · Types
Type hints: the contract7
Part 5 · The skeleton
The script skeleton8

Class definitions create objects: the class statement makes a new type, __init__ initializes each new instance, and self is the instance, passed automatically as the first argument of a method. From the Python tutorial, Classes, docs.python.org/3.

Python · No. 01
Objects

Classes: the blueprint

Three copy-pasted dicts become one type with a name

THE CLASS IS THE BLUEPRINT. EACH OBJECT IS ITS OWN HOUSE.the blueprintclass Server:instance s1host="eu-1"cpu=4its own attrsinstance s2host="us-1"cpu=2its own attrsinstance s3host="ap-1"cpu=8edited, others still safeone definition, three independent objects

Let's say the script tracks three servers as three copy-pasted dicts. The fourth has a typo in one key and the crash names the line, not the reason.

class Server: makes a type; def __init__(self, host): fills in each new object. self is the instance itself, passed for you. A missing self is the classic first error: TypeError about the argument counts.

The class is the blueprint. Each object is its own house.

TRY IT THIS WEEK

Make a Server class with __init__(self, host, cpu), build two, and change one attribute on one of them. Print both.

The try statement: try runs code, except handlers catch named exception classes, else runs when no exception occurred, finally runs on every exit. From the tutorial, Errors and Exceptions. docs.python.org/3.

Python · No. 02
Failures

try, except, else, finally

Four rooms, and each has one job

FOUR ROOMS. EACH HAS ONE JOB.try:the risky lineexcept Err:handles the named errorelse:no error happenedfinally: runs on every exit, error or notexcept:catches everything,even Ctrl-C tryingto stop the scriptname the error youexpect, let the restcrash loudly

The script reads a config that's sometimes missing. Half the code is checking, half is the work.

try holds the risky line; except names the error it can handle. else runs only when nothing broke, so the happy path leaves the try. finally runs on every exit, error or not. A bare except: catches everything, including the KeyboardInterrupt trying to stop you.

try the risk, name the error, keep the work in else.

TRY IT THIS WEEK

Wrap open("missing.txt") in try/except FileNotFoundError and print a clean message instead of a traceback.

Comprehensions: [expr for x in it if cond] builds a list, {k: v for ...} a dict, {expr for ...} a set, each with an optional filtering if. From the reference, Displays for list, set and dictionary. docs.python.org/3.

Python · No. 03
Data

Comprehensions

One line where the loop and its append used to be

ONE LINE WHERE THE LOOP AND ITS APPEND USED TO BE.beforeout = []for n in names: out.append(f(n))out = [f(n) for n in names]d = {n: f(n) for n in names}s = {n for n in names}[x for a in rows for x in a if x]stopped reading: a loop againthe same shape, three outputs:list [a, b, c]dict {a: 1, b: 2}set {a, b}

Every script has one: start a list, loop, append, return. Four lines doing one job, and the job's shape is buried in the middle.

[f.clean(n) for n in names if n] builds the list in one readable line. The same shape with braces makes a dict or a set. Two nested fors and a filter in one line stops reading; that's a loop again.

If it reads at a glance, it's a comprehension. If not, it's a loop.

TRY IT THIS WEEK

Replace one append loop in your code with [n.strip() for n in lines if n.strip()]. Count the lines you deleted.

Function annotations: def f(x: int) -> str records type hints that are stored but not enforced at runtime; they're consumed by static checkers like mypy. From the tutorial and typing docs. docs.python.org/3.

Python · No. 04
Types

Type hints: the contract

Read by your tools, ignored by the runtime

READ BY YOUR TOOLS. IGNORED BY THE RUNTIME.loader.pydef load(path: Path, limit: int) -> list[Row]: ...mypy, before running:load(cfg, "10")int expected, str giventhe runtime:load(cfg, "10")runs anyway. no check.the contract is read by tools, teammates, and you in six weeks

def process(rows, limit) tells you nothing at the call site six weeks later. List of dicts? Of strings? The docstring guesses.

def process(rows: list[str], limit: int) -> list[dict] is a readable contract. The runtime ignores it: it stores the hints and enforces nothing. mypy reads them and reports the wrong types before the code runs. Passing limit="10" still works at runtime, and still breaks two functions later.

The runtime ignores hints. Your reader doesn't.

TRY IT THIS WEEK

Annotate one function with types, then run mypy on the file (pip install mypy) and pass it one wrong argument on purpose.

The script skeleton: a docstring stating purpose, imports at the top, constants in one place, functions above a main() guarded by if __name__ == "__main__":. The ordering taught across the tutorial's module guidance. docs.python.org/3.

Python · No. 05
Craft

The script skeleton

Every page of this book, in one short file

EVERY PAGE OF THIS BOOK, IN ONE SHORT FILE, TOP TO BOTTOM.organizer.py"""Organize exports by day."""from pathlib import PathBASE = Path("exports")def main() -> None: ...if __name__ == "__main__": main()docstring + importsconstants in one placefunctions above mainthe guard, at the bottomthen:pytest passesmypy passesclone,pip install -r,run

Twenty-one moves, and you use maybe eight on every file. Here is the order they go in.

Docstring, imports, constants, functions, then main() under the __main__ guard. Check your arguments and paths before the work starts. Scripts that grow bottom-up end with imports between functions and constants nobody can find.

The docstring is the first line of documentation you'll ever write. Write it first, actually.

TRY IT THIS WEEK

Rebuild one script of yours as the skeleton: docstring, imports, constants, one function, main under the guard. Nothing else.

Index

Index


Classes: the blueprint4
Comprehensions6
The script skeleton8
try, except, else, finally5
Type hints: the contract7