Clean Code & Type Hints
- Rename variables and functions so the code reads as a description of intent
- Refactor nested conditionals into guard clauses and small single-purpose functions
- Annotate functions with type hints (list[str], int | None, TypedDict) and run mypy on them
- Apply DRY judgment: extract shared logic at the right moment, not the first repetition
- Run ruff to lint and format a module and fix every warning it raises
| Spaced-rep warm-up: due flashcards from Weeks 1β2 | 10 min |
| ELI5 + tech read: naming, guard clauses, hints, mypy model | 20 min |
| Guided: guard-clause surgery + mypy/ruff on your code | 35 min |
| Practice: the smell clinic on your Day 14 analyzer | 20 min |
| Project: full refactor pass, two commits | 25 min |
| Quiz + write flashcards | 10 min |
Builds on: Day 3 β Functions, scope & modules Β· Day 9 β Object-oriented Python I Β· Day 14 β Week 2 project β the log analyzer
Walk into two workshops. In the first, tools are wherever they last landed: three screwdrivers in a drawer labeled "misc," a saw under a tarp, nothing where the label says. The carpenter still builds good furniture β but every task starts with ten minutes of searching, and a visitor can't help at all. In the second workshop every drawer is labeled with exactly what it holds, each tool does one job, and the labels are honest: the drawer marked "wood screws, 40mm" contains wood screws, 40mm.
Code is a workshop that other people (including future-you) must work in. Naming is the labeling: days_until_expiry beats d. Small functions are single-purpose tools: a function called parse_line that also writes to a file has a lying label. Type hints are the printed drawer labels β def top_errors(lines: list[str]) -> dict[str, int] tells everyone what goes in and what comes out, and a robot inspector (mypy) walks the shop checking that nobody put a saw in the screw drawer. Tidiness isn't decoration; it's the speed of everyone who enters after you.
From here on, every project in this program β and every repo you touch professionally β gets read far more often than it gets written. AI engineering code is especially prone to rot: prompt strings, data-munging glue, and notebook-grown functions accumulate fast. Type hints are also load-bearing in the modern stack: pydantic (Day 41) validates API payloads from annotations, and FastAPI generates docs from them. In an FDE setting, a customer's engineers will read your prototype code the week after the demo β tidy, typed code is a trust signal you can't fake later.
Guided practice
Guard-clause surgery
15 min- Create
refactor_lab.pyand paste the BEFORE function from the starter code β read it and honestly time how long it takes you to say what it returns for a malformed line. - Refactor it in three passes, running it after each pass on the sample lines at the bottom: (a) invert the conditions into early returns; (b) rename
x,p, andresto intent-revealing names; (c) pull the timestamp parsing into its own small function. - Compare with the AFTER shape in the starter code β yours doesn't need to match exactly, but the happy path must sit at indentation level one.
- Write a one-line comment at the top: which pass helped readability most?
Type-check your own code with mypy and ruff
20 min- Terminal: install the inspectors into your environment:
pip install mypy ruff. - Add full type hints to
parse_lineβ the return type isdict[str, str | int] | None(or define aTypedDictnamedLogRecordfor extra credit). - Below it, deliberately write a caller that forgets the None case (see starter code), then run in the terminal:
mypy refactor_lab.py. Read the error β mypy caught a crash you never executed. - Fix the caller with an
if record is None:guard and rerun mypy until it reports success. - Terminal: run
ruff check refactor_lab.pythenruff format refactor_lab.py. Fix anything check reports and diff what format changed.
On your own
The smell clinic
20 minOpen your Day 14 log analyzer and find, in YOUR OWN code: (1) one name that hides intent β rename it everywhere; (2) one function doing two jobs β split it; (3) one nested conditional β flatten it with guard clauses; (4) one piece of duplicated knowledge (a repeated string, regex, or magic number) β give it one home as a named constant.
Constraints: behavior must not change β rerun the analyzer on the sample log before and after and diff the output. Finish by running ruff and mypy on the file.
Hints (only if stuck): magic numbers usually want to become module-level UPPER_CASE constants; a function that is hard to name is usually two functions.
Refactor the log analyzer, live
Do a full clean-code pass on the Day 14 log analyzer: intent-revealing names throughout, no function longer than ~25 lines, guard clauses instead of nesting, one home for every constant and regex, and complete type hints on every public function (including a TypedDict for the parsed record). The bar: ruff check reports zero issues, ruff format changes nothing, and mypy passes. Commit in two steps β one commit for renames/structure, one for type hints β with messages that say why, not what. This exact codebase becomes a tested, shipped package on Day 21, so today's tidiness is next week's speed.
Common mistakes & misconceptions
- Believing type hints change runtime behavior. Python ignores them when running; only tools like mypy and pydantic read them. A wrong hint is a lie that mypy exists to catch.
- Writing `list` or `dict` bare when you know the contents. `list[str]` and `dict[str, int]` let mypy check the elements too β the bare form checks almost nothing.
- Returning `str | None` but never guarding None at call sites. The hint's whole value is forcing that guard; suppressing the mypy error with a blind assertion defeats it.
- Extracting an abstraction on the first repetition. Duplication is cheaper to fix than the wrong abstraction β wait until the third occurrence proves the pattern.
- Renaming things in your head but not in the code ("I know what x means"). Six weeks from now you are the stranger in your own workshop.
- Treating linter output as noise to silence. Each ruff rule encodes a real bug class; disable one only with a comment saying why.
Q1. A function is annotated `-> str | None`. What does mypy demand from callers?
Q2. What does mypy actually do when you run it?
Q3. You notice the same 3-line snippet in two functions. The cleanest immediate move is usuallyβ¦
Go deeper β curated resources
- Run mypy in strict mode β Try mypy --strict on refactor_lab.py and fix what it adds. Strict mode is the default in many production codebases; knowing what it demands (no implicit Any, full annotations) is a hiring signal.
- refactor_lab.py passes mypy and ruff with the None-guard fix in place
- Analyzer refactor committed in two commits with identical before/after output
- mypy + ruff clean on the analyzer
- Quiz β₯ 2/3 (revisit and retake if lower)
β Back: Day 3 taught you to write functions; today you learned to make them legible. The analyzer you are polishing is the Day 14 project, and the class you typed traces back to Day 9.
Forward β: Day 18's tests and Day 21's shipped package assume this tidy, typed codebase. On Day 41 pydantic turns these same annotations into runtime validation for APIs, and on Day 110 typed schemas become the contract you hand an LLM.
Unlocks: D18 Testing I β pytest Fundamentals Β· D21 Week 3 Checkpoint: Ship a Tested Package