The beginner course taught you to write programs. This course is about the programs you’ll spend most of your time on: ones that already exist, work well enough that people depend on them, and need to change anyway.
All week you’ll grow one tool, a log analyzer. Servers write a line to a log for everything that happens, and someone always needs to know how many errors there were, and where. On Day 7 it will be a tested command-line tool that copes with messy input. Today it’s this:
Run it. It works. That’s worth saying, because the rest of today is about changing it, and “it works” is the thing you mustn’t lose along the way.
How to use this course
Seven days is a pace, not a deadline. Each day is a module, and the project carries over, so taking a day over two sittings costs nothing.
Core and “Going further”. Sections headed Going further are optional: nothing later in the course depends on them. Skip them on a first pass.
Help comes in steps. Every exercise has hints before its solution — the idea first, then a first step. Open only as many as you need. The same goes for AI assistants: ask for an explanation or a hint rather than the finished code, check what you’re told, then do a similar task without it. (The beginner course’s first lesson says more about that routine.)
Ready? A quick check
Three tasks using only the beginner course. They’re a readiness check, not a test: if one stops you, the lesson named in its hint is worth a reread before Day 2.
The beginner course marks four lessons Explore — optional there. This course uses all four: Iterators & Generators and Inheritance on Day 2, Arguments in Depth on Day 4, and Dunder Methods on Day 5. If you skipped them, read each before the day that needs it.
1. Predict both lines.
from collections import Counter
lines = ["ERROR db", "INFO web", "ERROR db", "WARN web", "ERROR api"]
levels = Counter(line.split()[0] for line in lines)
print(levels.most_common(1))
errors = sorted({line.split()[1] for line in lines if line.startswith("ERROR")})
print(errors)Check your prediction
[('ERROR', 3)]
['api', 'db']A generator expression feeding Counter, then a set comprehension that removes
the duplicate db. From the beginner lessons on Comprehensions and Modules.
2. Fix. Count the status codes, skipping any line that isn’t a status and a
path. It should print {'200': 2, '404': 1, '500': 1}, and it crashes.
lines = ["200 /home", "404 /missing", "200 /about", "oops", "500 /api"]
counts = {}
for line in lines:
status, path = line.split()
counts[status] = counts.get(status, 0) + 1
print(counts)Check your fix
lines = ["200 /home", "404 /missing", "200 /about", "oops", "500 /api"]
counts = {}
for line in lines:
try:
status, path = line.split()
except ValueError:
continue
counts[status] = counts.get(status, 0) + 1
print(counts){'200': 2, '404': 1, '500': 1}"oops" splits into one word, and unpacking one value into two names raises
ValueError. Catch the specific error and skip the line. From the beginner
Errors & Exceptions lesson — and Day 2 of this course is about doing this
properly.
3. Write top(counts, n): the n keys with the highest counts, ties in
alphabetical order. This should print ['api', 'db']:
# def top(counts, n): ...
print(top({"db": 3, "api": 3, "web": 1}, 2))Check your answer
def top(counts, n):
return sorted(counts, key=lambda key: (-counts[key], key))[:n]
print(top({"db": 3, "api": 3, "web": 1}, 2))['api', 'db']A tuple key — count descending, then name — and a slice. From the beginner Scope & Lambdas lesson.
What’s wrong with code that works
Nothing, until someone has to change it. Say you’re asked to count warnings per source as well as errors, or to read a different file, or to check that the error count is right. Try to do any of those in your head, and notice what gets in the way:
- The names say nothing.
c,e,w,s: you have to run the whole loop in your head to find out thatcis “count per level”. - Everything happens at once. Reading the file, splitting lines, counting and printing are all tangled in one loop. You can’t check the counting without also reading a file and printing.
- Nothing can be tested on its own. There are no functions, so there’s nothing to call with a small input and check the answer.
- The file name is baked in. It reads
sample.logand onlysample.log.
None of these is a bug. They are all reasons the next change is likely to add one.
Say what it does, precisely
Before changing code, write down what it does. Not what it’s meant to do: what it does. For this script:
- Reads
sample.log, and ignores lines that are empty or only whitespace. - Counts the lines it didn’t ignore.
- Counts lines per level, where the level is the third whitespace-separated word, in the order each level first appears.
- For
ERRORlines, counts per source, where the source is the fourth word with its last character removed. - Prints three lines, in that order, in that exact format.
Point 4 is worth a second look. w[3][:-1] removes the last character, whatever
it is. The author assumed it’s always the colon in payments:. That’s true for
every line in today’s file. Whether it’s true for every log you’ll ever see is a
question for later in the week. Today’s rule is stricter: don’t change
behaviour while restructuring. Notice it, write it down, and keep going.
Two kinds of change, never at once
A refactor changes how the code is organised and leaves what it does exactly the same. A fix or a feature changes what it does. Do them as separate steps. If you do both at once and the output changes, you can’t tell which of the two changed it.
Ask the code questions
Reading is faster when you run small experiments instead of simulating the code
in your head. What does w look like for one line? What does w[3][:-1] do to
a word that has no colon?
line = '2026-09-01 09:15:30 ERROR payments: order=1043 msg="card declined"'
w = line.split()
print(w)
print(w[2], "|", w[3], "|", w[3][:-1])
print("payments"[:-1]) # no colon: the last letter goes insteadA few lines like this answer questions that would take minutes of careful reading. Treat the interpreter as a way of reading code, not only a way of running it.
Today’s plan
You’ll change this script without changing what it prints, in three steps:
- Build a safety net. A test that fails if the output changes at all.
- Extract functions, one at a time, running the test after each step.
- Test the pieces. Now that there are functions, give each one tests of its own with small inputs.
At the end of the day the report prints exactly what it prints now, and every part of it can be tested and changed on its own. That’s what Day 2 builds on.
Read it like a reviewer
Without running anything, predict what the script prints if sample.log
contains only these two lines:
2026-09-01 10:00:00 ERROR db: timeout
2026-09-01 10:00:05 ERROR db:timeoutThen check your prediction by editing sample.log in the playground at the top
of the page and running it. (Reset puts the original file back.)
Hint 1: the idea
You don’t need to run anything: the numbered list in Say what it does, precisely describes the script exactly. Apply it to each line by hand. Point 4 is the one that decides the answer.
Hint 2: a first step
Write down the fourth whitespace-separated word of each line. Look closely at
the second line: there’s no space after the colon. Then apply [:-1] to each
word — it removes the last character, whatever that character is.
Show one solution
Lines read: 2
Levels: {'ERROR': 2}
Errors by source: {'db': 1, 'db:timeou': 1}The second line has no space after the colon, so its fourth word is
db:timeout, and [:-1] turns that into db:timeou. Same service, counted
twice, under a name that doesn’t exist. That’s point 4 from the list above,
showing up. It goes on your list of things to fix. It isn’t fixed during the
refactor, because a refactor mustn’t alter what the script does. Lesson 4 makes
one deliberate fix once the refactor is done. This one waits for Day 2, which
replaces the whole line-splitting approach, and the bug goes with it.
What you can now do
- Separate “what’s wrong with this code” from “what’s wrong with what it does”.
- Describe precisely what existing code does, including the parts that look like accidents.
- Use small experiments in the interpreter to read code faster.
- Keep restructuring and behaviour changes as separate steps.
Next: a test that will tell you, instantly, if anything you do changes the output.