Python module 3: functions, modules, libraries, debugging
Fourteen sessions to go from script to organised program: split code into functions, use the hundreds of modules shipped with Python, install external libraries cleanly, read a traceback without fear, and write your first real file automations.
Day 29 — Functions: def, parameters, return
Goal: create reusable blocks of code with inputs and an output.
Theory (20 min)
A function is a named block of code, defined once and called as many times as you like. You have used them since day 2: print(), len(), input() are functions written by others. Today you write your own:
def greet(name):
return f"Hello {name}!"
message = greet("Alice")
print(message)
print(greet("Bob"))
Three ideas. def + name + parentheses + colon, then an indented block: the definition — it does not run yet, it records the recipe. name between the parentheses is a parameter: a variable that will receive the value given at call time (the argument). And return is the exit: it hands a result back to the caller and ends the function — anything after a return inside the function is ignored.
The distinction that changes everything: print displays, return hands back. A function that displays cannot be reused inside a calculation; a function that returns a value can. Lasting reflex: functions compute and return; the main program displays.
Why split into functions? To avoid repeating yourself (a calculation written once, called ten times), to name the pieces (compute_total(basket) reads by itself), and to test each piece separately — that will be day 47.
What does a function without return give back?
- 0
- None
- An error
Answer
None: Python's "nothing" value. If a variable holds None when you expected a result, look for a forgotten return.
Difference between print and return inside a function?
- None, both display
- print returns the value, return displays it
- print displays on screen; return hands the value back to the calling code
Answer
Only return lets the result be reused: in a calculation, a test, a file.
Practice (30 min)
- Create
tools.py:
def price_with_vat(net_price, rate):
return net_price * (1 + rate)
def is_even(n):
return n % 2 == 0
print(f"{price_with_vat(100, 0.2):.2f}")
print(is_even(7))
- Success criterion: prints
120.00thenFalse— two functions defined at the top, called below, returning values used by the main program. - Remove the return from price_with_vat (put a print instead) and rerun:
Noneshows up in the f-string. You have just seen the forgotten-return bug — put it back. - Rewrite day 12's tip calculator with a function
tip_rate(service)that returns 0.15, 0.10 or 0.0. The main program barely changes — that is the point.
Agent tips: "split this 40-line script into named functions without changing what it prints": an excellent refactor to delegate. Trap: it may turn a print into a return and break the display. Proof: identical output before/after, character for character.
Recap (10 min)
Three lines: def / parameter / argument / return, print vs return, the None bug. Without notes: a function average(a, b) that returns the average, called three times.
Day 30 — Scope and default arguments
Goal: understand where a variable exists, and give parameters default values.
Theory (20 min)
A variable created inside a function exists only inside that function: that is its scope. It is local. From the outside, it does not exist; a variable of the same name outside is a different box:
def compute():
total = 42
return total
result = compute()
print(result)
print(total) outside the function would raise a NameError: total is local. That is a protection, not a constraint: a hundred functions can each have their own total variable without stepping on one another. To get a value out: return. To pass one in: a parameter. Everything goes through the doors.
Default arguments make parameters optional:
def greet(name, formula="Hello"):
return f"{formula} {name}!"
print(greet("Alice"))
print(greet("Bob", "Hi"))
First call: formula is "Hello"; second: the given argument replaces the default. And named arguments make calls readable: greet(name="Cloe", formula="Hey") — order no longer matters once you name them.
A variable created inside a function is visible…
- Only inside that function
- Everywhere in the file
- In functions called afterwards
Answer
It is local: outside, it does not exist. Values leave through return.
def f(a, b=2): return a * b — what does f(3) return?
- An error: an argument is missing
- 3
- 6
Answer
b takes its default value: 3 × 2 = 6.
Practice (30 min)
- Reproduce the scope example in the REPL: define the function, call it, then try
print(total)outside the function — read the NameError. - Create
discounts.py:
def final_price(price, discount=0.10):
return price * (1 - discount)
print(f"{final_price(80):.2f}")
print(f"{final_price(80, discount=0.25):.2f}")
print(f"{final_price(80, 0.50):.2f}")
- Success criterion: prints 72.00, 60.00, 40.00 — the same parameter, used by default, named, and positional.
- Write
def repeat(text, times=3):that returns the text repeated with spaces ((text + " ") * timesthen.strip()).repeat("ha", 4)must return "ha ha ha ha".
Agent tips: "why does my variable no longer exist outside the function?": the agent explains scope in one sentence. Trap: it suggests global — almost always a bad idea, refuse and use return. Proof: the program without global, passing values through return.
Recap (10 min)
Three lines: local scope, default argument, named argument. Without notes: a function total(price, quantity=1, vat=0.2) tested with one, two and three arguments.
Day 31 — Naming and documenting: docstrings
Goal: write functions a stranger (or you in a month) understands without reading them in detail.
Theory (20 min)
A program is reread ten times more often than it is written. Two clarity tools today.
Naming: a function is an action verb (compute_total, is_valid, read_file), a variable is a thing noun (total, scores in the plural for a list), a boolean reads like a question (is_even, has_licence). A name must answer "what does it hold / do?" without having to read the body.
The docstring: a triple-quoted string placed as the first line of a function, saying what it does, what it expects, what it returns:
def price_with_vat(net_price, rate=0.20):
"""Return the price including VAT from the net price and the VAT rate.
net_price: positive number, in euros.
rate: 0.20 for 20%. Returns a float.
"""
return net_price * (1 + rate)
It is not a comment: the docstring is stored in the function and reachable through help(price_with_vat) in the REPL — that is Python's official help system, the same one used by every library you will install.
Practical rule: a function fits on one screen and does one thing. If its docstring contains "and then… and next…", there are probably two functions to extract.
Where does a docstring go?
- Above the function, with #
- As the first line inside the function, in triple quotes
- After the return
Answer
First line of the function body, in triple quotes: help() finds it there.
Which function name is best?
- compute_average
- process
- function2
Answer
A precise verb that says what the function returns.
Practice (30 min)
- Go back to your functions from days 29-30: give each a two-line docstring (what it does, what it returns).
- In the REPL, paste a function, then call
help(price_with_vat). Your docstring is displayed: you have just documented like a professional. - Success criterion:
help()shows your docstrings without error. - Renaming exercise: here is deliberately badly named code — rewrite it with good names:
def f(x, y):
z = x * y
return z + z * 0.2
- (Expected shape:
def total_with_vat(unit_price, quantity)computingnet_totalthen returning the taxed amount. Check: f(10, 3) returns 36.0 in both versions.)
Agent tips: "write this function's docstring": perfect for an agent — it describes what it sees. Trap: it may describe what the function SHOULD do rather than what it does (bug included). Proof: a test call whose result contradicts or confirms the docstring.
Recap (10 min)
Three lines: verbs for functions, docstring vs comment, the "one function = one thing" rule. Without notes: rewrite def g(a): return a * 1.2 cleanly, docstring included.
Day 32 — import: the standard library
Goal: use the modules shipped with Python: math, random, datetime.
Theory (20 min)
Python comes "with batteries included": hundreds of modules — themed toolboxes — are installed with it. That is the standard library. You activate them with import:
import math
print(math.sqrt(25))
print(math.pi)
You reach a module's tools with a dot: module.tool. A frequent variant, from math import sqrt, imports one tool alone, usable without a prefix: sqrt(9). Stick to import module at first: each tool's origin stays visible when reading.
Three modules for today:
math: sqrt, pi, floor, ceil;random:random.randint(1, 6)a random integer between 1 and 6 included,random.choice(mylist)a random element,random.shuffle(mylist)shuffles in place;datetime:datetime.date.today()today's date,datetime.date(2026, 12, 25) - datetime.date.today()a gap in days.
A computer's randomness is pseudo-random: produced by a formula. For games and tests it is perfect; it must never be used for security (passwords, keys) — that is what the secrets module is for.
Where is the full list? docs.python.org/3/library. Browse it tonight like a catalogue: knowing a tool exists is enough; you look it up the day you need it.
After from math import sqrt, how do you call the square root?
- math.sqrt(9)
- sqrt(9)
- import sqrt(9)
Answer
from X import Y makes Y usable without a prefix.
Which module draws a random element from a list?
- math
- datetime
- random
Answer
random.choice(mylist). And random is never for security: that is secrets.
Practice (30 min)
- In the REPL:
import maththenmath.sqrt(2),import randomthenrandom.randint(1, 6)five times — a die. - Create
countdown.py:
import datetime
target = datetime.date(2026, 12, 25)
today = datetime.date.today()
left = (target - today).days
print(f"{left} days left before Christmas 2026.")
- Success criterion: the printed number of days is right — check it roughly on a calendar. Change the target date to your next birthday.
- Improve day 15's higher-or-lower game: replace
secret = 7withsecret = random.randint(1, 100), bound it 1-100, play a real round.
Agent tips: "which standard-library module does X?": the agent knows the catalogue by heart. Trap: it sometimes suggests an external library where the standard one is enough. Proof: demand "without installing anything" and an example that runs in the REPL.
Recap (10 min)
Three lines: import vs from-import, today's three modules, why random is not for secrets. Without notes: draw three first names at random from a list of five (random.choice in a loop).
Day 33 — pathlib: painless paths
Goal: handle files and folders with pathlib instead of hand-built strings.
Theory (20 min)
A path is a file's address: Documents/python/memo.txt. Historic trap: Windows writes Documents\python with backslashes, macOS/Linux with slashes — a hardcoded path only works on one system. The pathlib module fixes that: it represents a path as a Path object, portable by construction:
from pathlib import Path
folder = Path("Documents") / "python"
file = folder / "memo.txt"
print(file.exists())
print(file.suffix)
The / operator assembles the parts the right way for your system. The life-changing tools on a Path:
.exists()does the path exist;.is_file()/.is_dir()refine;.namethe file name,.stemthe name without extension,.suffixthe extension (.txt),.parentthe parent folder;folder.iterdir()lists a folder's content — a for loop over it and you visit every file;folder.glob("*.txt")keeps only the files matching the pattern (the star stands for anything);folder.mkdir(exist_ok=True)creates the folder without crashing if it already exists;file.read_text(encoding="utf-8")andfile.write_text("...", encoding="utf-8"): read/write in one line, no with — perfect for small files.
Renames and moves: file.rename(target) moves or renames (target = the complete new path). That is the building block of days 40-41's automations.
What is Path("photo.JPEG").suffix?
- ".JPEG"
- "photo"
- ".jpeg" in lowercase
Answer
The extension as-is, case included: remember .lower() before comparing.
Why prefer Path over "folder" + "/" + "file"?
- It is shorter to type
- pathlib's / picks the right separator for the system, and Path offers exists(), glob(), etc.
- Strings are forbidden for paths
Answer
Portability + built-in tools. A path is not text, it is a structure.
Practice (30 min)
- Create a
playgroundfolder with three files in it:photo1.jpg,notes.txt,photo2.png(any content, made in the editor). - Create
explore.py:
from pathlib import Path
folder = Path("playground")
for f in folder.iterdir():
print(f"{f.name} — suffix {f.suffix} — {f.stat().st_size} bytes")
print("\nImages only:")
for f in folder.glob("*.jp*g"):
print(f.name)
- Success criterion: the three files are listed with size and suffix, then only photo1.jpg comes out of the glob. Add a fourth file and rerun: nothing to change in the code — that is the strength of folder iteration.
- Add
Path("playground/archives").mkdir(exist_ok=True)then move notes.txt into it withPath("playground/notes.txt").rename("playground/archives/notes.txt"). Check in the file explorer.
Agent tips: "list the .pdf files of this folder": the agent writes the glob. Trap: glob("*.PDF") does not find lowercase .pdf — case matters. Proof: create a file of each case and count what the program sees.
Recap (10 min)
Three lines: why Path, iterdir vs glob, rename to move. Without notes: list a folder's .txt files and print the one with the longest name.
Day 34 — venv: one bubble per project
Goal: create a virtual environment and understand why you never skip it.
Theory (20 min)
Tomorrow you will install libraries written by others. The classic problem: project A wants version 1 of a library, project B wants version 2 — a single system-wide install cannot serve both. Python's solution: a virtual environment (venv), a folder holding its own isolated copy of Python and of the project's libraries. One bubble per project: what gets installed in the bubble does not exist outside it.
Creation (once per project), inside the project's folder:
python -m venv .venv
-m venv asks Python to run its venv module; .venv is the name of the created folder (the dot keeps it discreet; that is the convention, do not invent another).
Then activation, to redo at every work session:
- Windows (Command Prompt):
.venv\Scripts\activate - Windows (PowerShell):
.venv\Scripts\Activate.ps1 - macOS/Linux:
source .venv/bin/activate
The sign the bubble is active: (.venv) appears at the start of the terminal line. While it is there, python and pip point inside the bubble. deactivate leaves it. The .venv folder is never shared (it is big and specific to your machine): you share the list of libraries, tomorrow.
What is a venv for?
- Speeding Python up
- Isolating a project's libraries from other projects
- Hiding your code
Answer
Each project gets its bubble: no version conflict between projects.
How do you know the bubble is active?
- (.venv) appears at the start of the terminal line
- Python prints a message at launch
- A green light in the editor
Answer
The (.venv) prefix in the prompt. No prefix = pip installs elsewhere.
Practice (30 min)
- Create a
librariesfolder, move into it in the terminal. python -m venv .venv— a few seconds, a .venv folder appears.- Activate it with your system's command. Success criterion: the terminal line starts with
(.venv). - Check the isolation:
python -c "import sys; print(sys.prefix)"prints a path ending in.venv— your Python is indeed the bubble's. pip list: almost empty (pip and setuptools). That is the point: a fresh bubble holds nothing.deactivate, rerun the sys.prefix command: the system path is back. Reactivate for the rest of the path — from here to the end of module 4, everything happens in bubbles.
Agent tips: an agent coding in your project must work inside your bubble: tell it "activate .venv first". Trap: it runs pip install outside the bubble and pollutes the system. Proof: pip list inside the bubble shows the new library, and sys.prefix ends with .venv.
Recap (10 min)
Three lines: the problem venv solves, the two commands (create, activate), the visual sign of activation. Without notes: delete the .venv folder and recreate it — it is disposable, that is what it is for.
Day 35 — pip and PyPI: installing a library
Goal: install requests in the bubble, and freeze the dependency list.
Theory (20 min)
PyPI (Python Package Index, pypi.org) is the official shop for Python libraries: over 500 000 free packages. pip is the delivery driver, installed with Python:
pip install requests
(Bubble active, obviously.) pip downloads the package and those it depends on, and stores them in .venv. pip show requests prints version and description; pip list everything installed; pip uninstall requests removes.
Two professional gestures:
- Freeze the dependencies:
pip freeze > requirements.txtwrites the exact list of installed packages and versions into arequirements.txtfile. Anyone (including you on another machine) recreates the bubble identically withpip install -r requirements.txt. THAT file is what you share, never .venv. - Pick a popular package: on PyPI, check the date of the latest release and the user count. A package abandoned for five years is a risk; requests, downloaded millions of times a day, is a standard.
Security hygiene, one sentence: only install packages you have looked up and recognised, never a name copied from an unknown message — typos on package names are a classic trap (typosquatting: reqeusts instead of requests).
What do you share so a colleague gets the same environment?
- The zipped .venv folder
- The requirements.txt file produced by pip freeze
- The list of commands you typed
Answer
requirements.txt: light, portable, replayable. .venv gets recreated, it does not travel.
What is typosquatting?
- A pip bug
- A typo in your code
- A malicious package named almost like a famous one
Answer
Hence the rule: check the exact name on pypi.org before installing.
Practice (30 min)
- In yesterday's
librariesfolder, bubble active:pip install requests. pip list: requests is there, with its dependencies (urllib3, certifi… — pip installed the whole family).- Success criterion:
python -c "import requests; print(requests.__version__)"prints a version number, for instance 2.32.x. pip freeze > requirements.txtthen open the file: each line is a package pinned to its exact version.- Proof of replayability:
pip uninstall -y requests, check the import now fails, thenpip install -r requirements.txt— everything comes back. That is the move you will make on every new project.
Agent tips: "install requests": the agent will — check it did so INSIDE the bubble (pip list) and that it updated requirements.txt. Trap: those are the two most frequent omissions. Proof: destroy .venv, recreate it, pip install -r requirements.txt, run the program.
Recap (10 min)
Three lines: PyPI and pip, freeze/requirements, the anti-typosquatting rule. Without notes: install then uninstall a small package of your choice, requirements.txt updated at each step.
Day 36 — requests: talking to a web API
Goal: fetch data from a public API with requests.
Theory (20 min)
An API (application programming interface) is a web service's door meant for programs, not humans: instead of a pretty HTML page, it returns raw data, most often in JSON (tomorrow). Your browser makes GET requests when you visit a page; requests does the same from Python:
import requests
response = requests.get("https://api.github.com", timeout=10)
print(response.status_code)
Three things to know in the response:
status_code: the HTTP verdict. 200 = success, 404 = unknown address, 403 = refused, 500 = the server crashed. Always check it before using the data;.text: the raw content, as text;.json(): the content decoded into Python dicts and lists — day 37's gift;- and the
timeout=10argument: without it, a request can wait on the network forever. Always a timeout.
The network is moody: you need internet, the service may be down, addresses change. A program that talks to the network must always handle failure politely — we will combine try/except and status_code in practice.
A status_code of 404 means…
- Success
- Address unknown to the server
- The server crashed
Answer
404: the requested resource does not exist at that address. 200 is success.
Why always pass timeout= to requests.get?
- Otherwise the program can wait on the network forever
- To speed the request up
- It is a syntax requirement
Answer
Without a timeout, a silent network blocks your program without end.
Practice (30 min)
- Bubble active, requests installed. In the REPL: the lesson's request, check the 200, then
response.text[:100]to see the start of the content. - Create
weather.pywith the open wttr.in API (no account, no key):
import requests
city = input("City? ")
url = f"https://wttr.in/{city}?format=3"
response = requests.get(url, timeout=10)
if response.status_code == 200:
print(response.text)
else:
print(f"Error {response.status_code}")
- Success criterion: type "Lyon": a line like "Lyon: ☀️ +18°C" (depending on real weather) prints. Type a nonexistent city: your else answers cleanly.
- Switch the wifi off, rerun: requests raises a connection error. Add
try: ... except requests.RequestException:around the get to print "network unreachable" instead of crashing. Plug back in.
Agent tips: "call this API and extract field X": the agent writes the request, but it sometimes invents fields that do not exist in the real response. Classic trap. Proof: demand a print of the raw response (or response.json()) and check the fields yourself.
Recap (10 min)
Three lines: what an API is, status_code always checked, timeout mandatory. Without notes: a GET request, a 200 check, print the text, a network except.
Day 37 — json: the format of APIs
Goal: read and write JSON, the web's exchange format.
Theory (20 min)
JSON (JavaScript Object Notation) is the text format in which services exchange data: it looks like a Python dictionary — braces, brackets, colons — on purpose:
{"name": "Alice", "age": 30, "languages": ["fr", "en"]}
Differences from Python: strings are double-quoted only, and the special values are written true, false, null (lowercase) instead of True, False, None.
The standard library's json module translates both ways:
json.loads(text): JSON text → Python objects (loads = load string);json.dumps(obj): Python objects → JSON text (dumps = dump string);- and requests does the first direction by itself:
response.json().
After decoding, you are back with your old friends from modules 1 and 2: dicts and lists, read with brackets. An API response is often a big nested dict: walk it floor by floor, data["main"]["temp"], printing the keys at each floor (data.keys()) to find your way.
To save Python data into a file: json.dumps(data, indent=2, ensure_ascii=False) produces readable text, indented, with accents intact — the ideal format for your programs' small saves.
Difference between json.loads and json.dumps?
- None, same function
- loads: JSON text → Python objects; dumps: Python objects → JSON text
- loads reads a file, dumps writes it
Answer
The trailing 's' means "string": both work on text.
In JSON, the "nothing" value is written…
- None
- empty
- null
Answer
null, lowercase. json.loads turns it into None.
Practice (30 min)
- In the REPL:
import json, thenjson.loads('{"a": 1}'), thenjson.dumps({"first": "Cloe"}, ensure_ascii=False). - Create
repos.py— GitHub's public API, no account:
import requests
url = "https://api.github.com/repos/python/cpython"
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
print(f"Project: {data['full_name']}")
print(f"Stars: {data['stargazers_count']}")
print(f"Language: {data['language']}")
else:
print(f"Error {response.status_code}")
- Success criterion: three real lines print (project python/cpython, a large star count, language Python). Change the URL to another repository you know.
- Explore: add
print(sorted(data.keys()))to see every available field, pick another one (description?) and print it. - Save: write
dataintorepo.jsonwith json.dumps + Path.write_text, reopen the file in the editor: it is readable JSON.
Agent tips: "extract fields X, Y from this JSON response": paste the REAL response (or its beginning) — otherwise the agent invents the structure. The number one trap of API work. Proof: its data["..."] accesses must raise no KeyError on the real response.
Recap (10 min)
Three lines: JSON vs Python dict, loads/dumps, walking floor by floor. Without notes: read an API response, extract two fields, save as indented JSON.
Day 38 — Reading a traceback without fear
Goal: decipher a full traceback: the call stack, the faulty line, the cause.
Theory (20 min)
On day 7 you learned to read a simple error message. With functions, errors travel across floors: the traceback tells the whole journey. Example:
Traceback (most recent call last):
File "app.py", line 12, in <module>
show_total("abc")
File "app.py", line 8, in show_total
total = add(price)
File "app.py", line 4, in add
return float(a) + float(b)
~~~~~^^^
ValueError: could not convert string to float: 'abc'
Reading, always bottom to top. The last line: the error type and the precise cause ('abc' cannot be converted). The line above: the exact place it blew up, file + line number + the faulty code, with carets ^^^ under the culprit (a welcome addition of Python 3.11+). The lines on top: the call stack — who called whom to get there, from the main program down to the guilty function.
The professional reflex: the error is rarely fixed where it explodes. Here, float("abc") is innocent: it is the caller, line 12, that passed a bad value. Walk up the stack until you find where the faulty data comes from.
And the technique that saves hours: copy the traceback's last line as-is into a search engine. You are never the first to meet it.
In a traceback, where is the cause?
- At the very bottom: the error type and its message
- At the very top
- In the middle of the stack
Answer
Most recent call LAST: the end tells the error, the top tells the journey.
Is the function where the error explodes always the guilty one?
- Yes, always
- No: often the caller supplied the bad data — walk up the stack
- Only for ValueError
Answer
The stack shows who called whom: the faulty data often comes from a floor above.
Practice (30 min)
- Create
broken.py, with a real three-function stack:
def convert(text):
return float(text)
def average(a, b):
return (convert(a) + convert(b)) / 2
def main():
print(average("10", "fifteen"))
main()
- Run it. Success criterion: without help, you identify in the printed traceback: the last line (ValueError), the faulty line (convert), and the origin of the bad data (main, passing "fifteen").
- Fix at the source ("15"), rerun: 12.5.
- Fix differently: keep "fifteen" and add a try/except ValueError in convert that returns 0.0 — watch the program survive, and ask yourself whether that is a good idea (a silent 0 skews the average: sometimes crashing is better).
Agent tips: paste the WHOLE traceback plus the code: "quote the faulty line and the line that handed it the bad data". Trap: without the code, it guesses the most common cause, not yours. Proof: its fix must make the traceback disappear on your machine.
Recap (10 min)
Three lines: traceback structure (cause / place / stack), why walk up the stack, the web-search trick. Without notes: provoke a two-floor KeyError and narrate its traceback out loud.
Day 39 — Debugging: the strategic print and pdb
Goal: find a bug that does not crash — a wrong result — with the diagnostic print and the debugger.
Theory (20 min)
The hardest bug is not the one that crashes: it is the one that returns a wrong result in silence. No traceback, just a bad number. The method has a name: divide and conquer. Cut the program in half with a probe, see which side the lie starts on, cut again, until the faulty line.
The simplest probe is the diagnostic print, with a prefix that sets it apart:
def compute_total(price_list):
total = 0
for price in price_list:
total = total + price
print(f"DEBUG total={total}")
return total
You watch the intermediate values, find where it goes off the rails, then remove the probes. It is rudimentary and it solves most everyday bugs.
The built-in tool for stubborn cases: pdb, Python's debugger. Insert breakpoint() in the code, run the program: it freezes at that line and gives you a (Pdb) prompt where you inspect at leisure: type a variable's name to see its value, n (next) to step one line, c (continue) to resume, q to quit. It is a REPL in the middle of the execution.
The program returns a wrong result without crashing. First step?
- Rewrite everything
- Ask an AI
- Place a probe (DEBUG print or breakpoint) in the middle and check the values
Answer
Divide and conquer: locate the faulty half before hunting the line.
In pdb, what does the n command do?
- Runs the next line and stops
- Quits the debugger
- Prints the value of n
Answer
next: one more line. c continues to the next breakpoint, q quits.
Practice (30 min)
- Create
bug.pywith this deliberately wrong program (it should average the positive prices):
def average_positives(prices):
total = 0
for p in prices:
total = total + p
return total / len(prices)
print(average_positives([10, -5, 20]))
- Run it: prints 8.33… but the average of the positives (10 and 20) is 15. The program summed the -5 and counted three prices. Find it with probes, not by rereading: place
print(f"DEBUG p={p} total={total}")in the loop, watch, then fix (you must filterif p > 0and count the positives separately). - Success criterion: the fixed version prints 15.0.
- Replay the same hunt with pdb: restore the bug, insert
breakpoint()before the loop, run, step withnwhile printingpandtotaleach round,qto exit. Same verdict, another tool.
Agent tips: "expected result is 15, I get 8.33, here is the code": giving expected vs observed is half the agent's job. Trap: without the expected result, it "improves" the code without fixing the bug. Proof: the number 15.0 after its fix.
Recap (10 min)
Three lines: divide and conquer, DEBUG print, breakpoint()/n/c/q. Without notes: fix an accumulator that forgets the first element (wrongly initialised to 1) in one probe hunt.
Day 40 — Automation: renaming files in bulk
Goal: rename dozens of files in one go, with a dry-run mode before any move.
Theory (20 min)
First real automation: rename a collection of files by a rule. The textbook case: photos named IMG_0001.JPG that you want as holiday-01.jpg. Everything is already known: iterdir (day 33), string methods (day 19), rename (day 33).
The safety rule that separates a useful script from a disaster: the dry-run. The program first prints what it would do — old name → new name — without touching anything; you check; you rerun in real mode. Any script that moves, renames or deletes deserves a dry-run.
Second guardrail: work on a copy. Experiments happen in a test folder full of fake files, never on the real photos.
Two string methods handy for names: .zfill(2) pads a number with zeros ("3".zfill(2) → "03"), and joining stem + new suffix is clean with Path's attributes: f.stem, f.suffix.
Practice (30 min)
- Build the playground: create a
photosfolder, then generate fake files by script (let the machine work for you):
from pathlib import Path
folder = Path("photos")
folder.mkdir(exist_ok=True)
for i in range(1, 6):
(folder / f"IMG_000{i}.JPG").write_text("fake", encoding="utf-8")
- The renamer,
rename.py:
from pathlib import Path
folder = Path("photos")
real_mode = False # switch to True to apply
photos = sorted(folder.glob("*.JPG"))
for i, f in enumerate(photos, 1):
new = folder / f"holiday-{str(i).zfill(2)}{f.suffix.lower()}"
print(f"{f.name} -> {new.name}")
if real_mode:
f.rename(new)
- Success criterion (dry-run): the program prints five mapping lines
IMG_0001.JPG -> holiday-01.jpgand the folder has not changed — check with the file explorer. enumerate(photos, 1)is new: each round gives the index (starting at 1) AND the element — no more manual counter.- Switch
real_mode = True, rerun, check the folder. Set it back to False. - A trap to observe: rerun the dry-run now: glob("*.JPG") finds nothing (everything is .jpg). A good script reports "nothing to do" instead of staying silent: add
if not photos: print("No file found.").
Agent tips: "rename these files by this rule": exactly the kind of task to delegate — with a dry-run REQUIRED in the brief, and on a copy. Trap: an agent launched without a dry-run can rename 300 files with a wrong rule. Proof: the planned list, displayed, proofread, before a single rename.
Recap (10 min)
Three lines: dry-run, enumerate, zfill. Without notes: a script that prefixes every .txt of a folder with "read-", dry-run first.
Day 41 — Automation: tidying a folder
Goal: sort a folder into subfolders by file type — the base of the final project.
Theory (20 min)
Everyone's Downloads folder: 200 mixed files. The tidying rule: images into images/, PDFs into documents/, archives into archives/, the rest into misc/. Translation into known structures: an extension → subfolder dictionary (day 21), a folder iteration (day 33), rename to move (day 33), dry-run (day 40).
RULES = {
".jpg": "images", ".jpeg": "images", ".png": "images",
".pdf": "documents", ".txt": "documents",
".zip": "archives",
}
For each file: destination = RULES.get(f.suffix.lower(), "misc") — day 22's .get with default, filing everything unplanned into "misc". The .lower() absorbs day 33's .JPG files.
One real-world robustness detail: name collisions. If documents/invoice.pdf already exists, rename silently overwrites it on Linux. Before moving, test target.exists(); if so, add a suffix -1, -2… to the stem until a free name shows up. Five lines that avoid losing a file.
Practice (30 min)
- Playground: a
downloadsfolder + a generation script for 8 varied fake files (2 .jpg, 1 .JPG, 2 .pdf, 1 .zip, 1 .mp3, 1 .txt). - Write
tidy.py(alone from the plan above; help if stuck: iterdir loop, filterif f.is_file(), get on RULES, mkdir(exist_ok=True) on the subfolder, target = subfolder / f.name, print then rename in real mode). - Success criterion (dry-run): eight mapping lines, each to the right subfolder, the uppercase .JPG filed in images/, the .mp3 (unplanned) in misc/.
- Real mode, check the folder: eight files tidied into four subfolders.
- Collision test: recreate an
invoice.pdfat the root whiledocuments/invoice.pdfexists, add thetarget.exists()handling with suffix, rerun: you getdocuments/invoice-1.pdf, nothing is overwritten.
Agent tips: this script is the favourite of agent demos; the trap is always the same: it overwrites duplicates without warning. Demand collision handling in the brief. Proof: the step-5 test, which must produce invoice-1.pdf without losing the original.
Recap (10 min)
Three lines: extension → folder dict, get with default, the danger of collisions. Without notes: rule variants (by date? by size?) — think about what would make the tool useful FOR YOU: that is the seed of the final project.
Day 42 — Module 3 review
Goal: prove the module's skills: functions, modules, venv/pip, traceback, automation.
Theory (20 min)
Reread your notes from days 29 to 41. The achievement table:
| Notion | Shape | Session |
|---|---|---|
| Function | def / parameters / return / docstring | D29-31 |
| Import | import math, random, datetime, pathlib, json | D32-33, 37 |
| Project bubble | venv + activate + pip + requirements.txt | D34-35 |
| Network | requests.get + status_code + timeout + .json() | D36-37 |
| Errors | traceback bottom-up, DEBUG print, breakpoint() | D38-39 |
| Automation | iterdir + glob + rename + dry-run + collisions | D40-41 |
One fuzzy notion = one session to redo before the review, not after.
Practice (30 min)
The review program, watch.py, written alone: a script that queries an API and archives the response.
- In a fresh folder with its bubble (venv, pip install requests, freeze):
- Write a function
fetch(url)that does the GET with a timeout and returns the JSON dict, or None if the request fails (try/except + status_code). Docstring included. - Write a function
save(data, path)that writes the indented JSON into a dated file (watch-2026-11-03.jsonwithdatetime.date.today()and an f-string). - Main program: call
https://api.github.com/repos/python/cpython, extract three fields into a small dict (name, stars, description), save.
Success criterion: the dated JSON file exists, is readable in the editor, and the program runs twice without error while handling the network-off case (test with wifi off: polite message, no traceback). Every brick comes from a session of this module — if one resists, go back to its session.
Agent tips: have your review proofread: "find two fragilities in this script, quote the lines, rewrite nothing". Trap: it lists cosmetic improvements instead of real risks (network, encoding). Proof: each quoted fragility must be triggerable by a concrete test.
Recap (10 min)
Three lines: what you can do now that was impossible on day 28. Module 4 tomorrow: we assemble everything into a finished, tested, documented project.
Found this useful? Tell us what to measure next.
Comments
No account, no tracker: a comment is read by a human before it appears.