We closed module 7 with an uncomfortable diagnosis: EasyTask v0.17 works, it is neatly split into modules and it does not have a single import cycle, but it is not documented beyond a few scattered docstrings. If tomorrow Marta hires someone and hands them the easytask/ folder, that person will have to read all five files from top to bottom to work out what the program does, how it is started and what it means for a task to have days. And you do not need to imagine a stranger: you in six months' time are exactly that stranger, with the added disadvantage of believing you remember. This lesson turns the package into something you can understand in five minutes: comments that explain the why and not the what, real docstrings following the official convention, type annotations that document and help your editor at the same time, and a README.md that is the project's front door. Documenting is not writing a lot: it is writing what the code cannot say on its own.
Contents
- Who you document for
- Comments: the golden rule
- Comments that lie, and
TODO/FIXMEmarkers - Docstrings: the PEP 257 convention
- The three styles: Google, NumPy and reStructuredText
- Type annotations: executable documentation
- The project's
README.md CHANGELOG.md, semantic versioning and tools- EasyTask v0.18: the documented package
- Common mistakes and tips
- Exercises
- Conclusion
- Who you document for
Before writing a single line it pays to be clear about who you are writing it for, because every reader needs something different:
| Reader | What they need to know | Where they look for it |
|---|---|---|
| Someone who uses the program | What it does, how to install it, how to start it | README.md |
| Someone who calls one of your functions | What it takes, what it returns, what can fail | Docstring and annotations |
| Someone who modifies the code | Why it is done this way and not another | Comments at the exact spot |
| You in six months | All of the above, and you have forgotten it | All three places at once |
That is where the lesson's three tools come from, and each has its place: the README explains the project from the outside, the docstrings explain each piece to whoever is going to use it, and the comments explain concrete decisions to whoever is going to touch that line. Confusing them is the first mistake: a two-hundred-line README is no substitute for a docstring, and a comment inside a function will never be read by someone who only wants to call it. And a warning that runs through the whole lesson: the best documentation is code that does not need any; before writing a comment that explains a confusing stretch, ask yourself whether it would be better to rename a variable or extract a function with a clear name, something we will come back to in Style, readability and refactoring.
- Comments: the golden rule
You have known the syntax since module 2: # turns everything from that point to the end of the line into a comment, and Python ignores it completely. It can take up a whole line or sit at the end of a line of code. What is not obvious is what to write inside, and there the golden rule is this: the code says the what; the comment says the why. The interpreter already tells with total precision what happens; what it cannot tell is the decision, the constraint or the surprise behind it.
# USELESS COMMENTS: they repeat what the code already says
counter = counter + 1 # increase the counter by one
if task.completed: # if the task is completed
done.append(task) # add it to the done list
# VALUABLE COMMENT: it explains the why
# The team works Monday to Friday: 5 real days per calendar week.
# Without this adjustment, a 10-day task landed on a Saturday in Marta's report.
weeks = days / 5The first three comments add nothing: anyone who knows Python works them out by reading the line, and they age badly on top of that. The last one holds information that is nowhere in the code: an Alba Studio business rule that took an afternoon to discover. If you delete it, that information disappears from the universe. Cases where a comment is nearly always worth it:
- Business rules a reader cannot guess (the 5 working days, the discount for client Vidal) and rejected decisions: "linear search was used on purpose: the list never goes above 200 tasks".
- Tricks forced by an external limitation: an odd file format, a quirk of a library.
- Warnings, formulas and units: "do not change the order of these two lines", or where a number comes from and what unit it is in.
- Comments that lie, and
TODO/FIXME markers
TODO/FIXME markersThere is something worse than a missing comment: a comment that lies. The code gets changed and the comment stays as it was, and from then on it actively misleads whoever reads it.
# Returns the high-priority tasks <-- LIE: it no longer filters by priority
def pending(agenda):
return [t for t in agenda if not t.completed]Whoever reads that comment will trust it and go looking for the bug somewhere else. The discipline is simple and non-negotiable: when you change a line, you review its comment; and between a dubious comment and none at all, none is better. This risk is another argument in favour of type annotations and good names: they do not drift out of sync so easily because they are part of the code. For pending work there is, in addition, a universal convention that every editor recognises and highlights:
| Marker | Meaning | Example |
|---|---|---|
TODO |
Something is missing, but nothing is broken | # TODO: allow editing the assignee |
FIXME |
Something is wrong and will need fixing | # FIXME: if days is 0 the average blows up |
HACK |
A deliberate, ugly stopgap | # HACK: we sort twice to work around an export bug |
NOTE |
Important warning for the reader | # NOTE: this file is read by Marta's script |
In VS Code they show up highlighted and you can list them all with a project-wide search for TODO. Write them as a concrete sentence — # TODO: validate the email works; # TODO: improve this does not — and review them from time to time: a project with forty TODOs from two years ago is a project where nobody reads them.
- Docstrings: the PEP 257 convention
A docstring is a text string placed as the first statement of a module, a class or a function. Unlike a comment, Python keeps it in the object's __doc__ attribute, so it can be consulted at run time. We have been using them in passing since 04-01; now we do them properly. The official convention is PEP 257, and its practical rules are these:
- They are always written with triple quotes (
"""), even when they take a single line, and the first line is a short summary in the imperative ending with a full stop: "Return...", "Calculate...", "Save...", never "This function returns...". - If there is more text, leave one blank line after the summary, and put the closing quotes on their own line.
def remaining_days(task):
"""Return the days left to finish the task.
Never returns a negative number: if the team has spent more days
than planned, the result is 0.
"""
return max(0, task.days - task.done)Docstrings go in four places, and each one answers a different question:
| Where | What it must tell |
|---|---|
Module (first line of the .py) |
What the file contains and what it is for |
Package (in the __init__.py) |
What the project is and which modules make it up |
Class (under the class line) |
What it represents, its attributes and its typical use |
Function or method (under the def) |
What it does, what it takes, what it returns and what can fail |
And this is how you read them, without leaving the interpreter: help(Task.complete) shows the signature and the docstring already formatted, Task.complete.__doc__ returns the raw string and help(easytask.model) presents the documentation for the whole module. That is the real reason to write them: help() works on your code exactly as it does on help(str.upper). If the docstring is well written, whoever uses your module does not need to open the file.
- The three styles: Google, NumPy and reStructuredText
For functions with several parameters you need some structure. There are three widespread conventions; they all say the same thing and differ only in form. Here is the same function in all three:
# --- Google style: the most readable in plain text ---
def filter_by(tasks, field, value):
"""Return the tasks whose field matches the given value.
Args:
tasks (list[Task]): Collection of tasks to filter.
field (str): Attribute name, for example "assignee".
value (str): Value to look for, case-insensitive.
Returns:
list[Task]: The tasks that meet the condition.
Raises:
AttributeError: If the field does not exist on Task.
"""
# --- NumPy style: sections underlined with dashes ---
"""
Parameters
----------
tasks : list[Task]
Collection of tasks to filter.
field : str
Attribute name, for example "assignee".
Returns
-------
list[Task]
The tasks that meet the condition.
"""
# --- reStructuredText style (classic Sphinx): fields with colons ---
"""
:param tasks: Collection of tasks to filter.
:returns: The tasks that meet the condition.
"""| Style | Look in plain text | Verbosity | Where you see it most |
|---|---|---|---|
| Very readable as is | Low | General projects, modern Python | |
| NumPy | Readable, takes more room | Medium | Data science: numpy, scipy, pandas |
| reStructuredText | Noisy unrendered | High | Older projects with Sphinx |
Recommendation for this course: the Google style. It is the one that reads best without tools, the shortest and the one that gets in the way least while you are coding. But what matters is not which one you pick, it is being consistent: a project with three styles mixed together is more confusing than one with no documentation at all. Pick one, note it in the README and stick to it. A practical detail: with type annotations you can drop the types in brackets, because they are already in the signature; field (str): becomes field:.
- Type annotations: executable documentation
Type annotations (type hints) declare which type is expected in each parameter and which type is returned. They are written with a colon after the parameter and an arrow -> before the body:
def summary(title: str, days: int, urgent: bool = False) -> str:
"""Return a summary line for the listing."""
return f"{'!' if urgent else ' '} {title} ({days} days)"You read it like this: title is a str, days an int, urgent a bool defaulting to False, and the function returns a str. For the structures from module 5 you also state what they contain:
| Annotation | Meaning |
|---|---|
list[str] |
List of strings |
dict[str, int] |
Dictionary with text keys and integer values |
tuple[str, int] |
Tuple of exactly two elements: a text and an integer |
str | None |
A text or None (typical of a search that may fail) |
list[Task] |
List of objects of your own class; -> None: returns nothing useful |
def find(tasks: list[Task], title: str) -> Task | None:
"""Return the first task with that title, or None if there is none."""The Task | None on find is especially valuable: it warns the reader that they must check for None before using the result, a warning that without the annotation would live only in the head of whoever wrote the function. And now the essential part: Python does not check annotations at run time. The call summary(title=42, days="three") raises no error because of the annotations; the program carries on until it breaks further in, with a message that no longer points at the culprit. Annotations are documentation your editor understands: VS Code autocompletes the methods and underlines the mistake before you run anything. If you want real checking there is mypy, an external tool that analyses the code and reports inconsistencies (pip install mypy, then mypy easytask/). We will come back to it in 08-05 with the rest of the quality tools.
- The project's
README.md
README.mdThe README.md goes in the project root and is the first thing anyone opens (GitHub and GitLab display it automatically, as we will see in 08-03). It must answer, in this order: what is this, what do I need, how do I install it, how do I use it, how is it organised and what can I do with it. The essentials of Markdown fit in one table:
| Syntax | Result |
|---|---|
# Title / ## Section |
Level 1 and level 2 headings |
**bold** / *italic* and - item / 1. item |
Emphasis and lists |
`code` and blocks with ``` |
Inline and block code |
[text](url) and rows with | |
Link and table |
And this is EasyTask's README:
# EasyTask Command-line task manager for the Alba Studio team. It lets you create tasks, assign them to a team member, mark them as completed, filter them, sort them by priority and save them to a JSON file. ## Requirements - Python 3.10 or later (it uses `match`/`case` and the `str | None` syntax). - No external libraries: everything is standard library. ## Installation and use
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate python -m easytask # from the package folder
Tasks are saved automatically to `tasks.json`. Option 9 exports a `tasks.csv`. ## Structure - `model.py`: classes `Task` and `RecurringTask`. - `agenda.py`: class `Agenda`, the collection and its operations. - `storage.py`: save and load JSON, export to CSV. - `interface.py`: menu and input/output. `__main__.py`: the `main()`. ## Conventions and licence - Google-style docstrings. Valid priorities: `high`, `medium`, `low`. - Team: Marta, Luis and Nuria (`TEAM`). Internal use at Alba Studio.
Notice what it does not have: it does not explain how Agenda.sorted_tasks() works inside — that is the docstring's job — nor does it tell the project's history. A README is measured by how fast someone goes from opening it to having the program running.
CHANGELOG.md, semantic versioning and tools
CHANGELOG.md, semantic versioning and toolsNext to the README there usually lives a CHANGELOG.md: the list of changes for each version, the most recent at the top. It answers "what has changed since the version I had?" without reading the whole history. And the version numbers this course has been using since module 3 are not arbitrary: they follow semantic versioning, MAJOR.MINOR.PATCH.
| Part | When it goes up | Example in EasyTask |
|---|---|---|
| MAJOR | A change that breaks previous usage | 0.x → 1.0: the project is considered stable |
| MINOR | New, compatible functionality | 0.17 → 0.18: documentation is added |
| PATCH | A bug fix, with no change in usage | 1.0 → 1.0.1: a calculation is fixed |
By convention, any version starting with 0 means "this can still change in any way", which is exactly EasyTask's situation throughout the course; at the end of the module v1.0 will arrive. A CHANGELOG entry is as simple as this:
## [0.18] - 2026-08-05
### Added
- Module, class and function docstrings across the whole package.
- Type annotations on the public functions, README.md and CHANGELOG.md.These are all text files, but they can be exploited automatically. Python ships with pydoc, which reads your docstrings and presents them already formatted: python -m pydoc easytask.model shows them in the terminal, python -m pydoc -w easytask.model generates an HTML file and python -m pydoc -b opens a browser with the whole project. It is the immediate reward for having written proper docstrings: with nothing installed, you have your package documented and browsable. For large projects there are Sphinx (the generator behind Python's official documentation) and MkDocs (simpler, built from Markdown files), which produce full websites with search and an index; they are far beyond what EasyTask needs, but they feed on the very docstrings you have just learned to write.
- EasyTask v0.18: the documented package
We apply it all to the project: a module docstring in every file, full docstrings on the classes, annotations on the public functions and the README you have already seen.
"""Data model of EasyTask.
Defines the Task class and its specialisation RecurringTask, along with the
domain constants. It depends on no other module of the package and prints
nothing: it can be reused from a website or from a script.
"""
class Task:
"""A task at Alba Studio with its assignee and its estimate.
Attributes:
title: Short description, for example "Book fair poster".
assignee: Name in lowercase, one of TEAM.
priority: One of PRIORITIES ("high", "medium" or "low").
days: Estimated working days; always greater than 0.
done: Days already spent.
completed: True if the task is finished.
"""
def __init__(self, title: str, assignee: str, priority: str = "medium",
days: int = 1) -> None:
"""Create a task, normalising the assignee and the priority."""
def percentage(self) -> float:
"""Return the progress percentage, between 0.0 and 100.0."""
# days is always > 0 thanks to validation, so there is no division by zero
return min(100.0, self.done / self.days * 100)In agenda.py the class docstring also includes a usage example, which is what newcomers appreciate most; and the __init__.py documents the whole package and carries the version number:
class Agenda:
"""Set of team tasks, with search, filtering and sorting.
The internal list is private: you access it by iterating over the agenda
(`for task in agenda`) or through the public methods.
Example:
>>> agenda = Agenda()
>>> agenda.add(Task("Sole Bakery logo", "nuria", "high", 5))
>>> len(agenda)
1
"""
# --- easytask/__init__.py: the package docstring ---
"""EasyTask: task manager for Alba Studio.
Modules: model (Task and RecurringTask), agenda, storage and interface.
Usage: python -m easytask
"""
__version__ = "0.18"Now python -m pydoc -b opens a page with the whole package explained, and whoever receives the folder knows in five minutes what it is, how it is started and what each file does. EasyTask v0.18: same behaviour, understandable project.
Common Mistakes and Tips
- Commenting the what instead of the why, or leaving comments that lie.
counter += 1 # add oneis noise; and when you change a line you must review its comment, because an out-of-date one does more damage than none. - Using
#where a docstring belongs. A comment above thedefis not picked up byhelp()orpydoc; a docstring inside thedefis. - Third-person docstrings, single quotes, or three styles mixed. Triple quotes always, an imperative first line ("Return...", not "This function returns...") and a single style (Google) noted in the README.
- Believing type annotations validate anything. They do not: they are documentation; to validate for real you need checks in the code, or
mypyoutside it. - Writing a README that starts with the installation. Start with a sentence that says what the program is: some people will only read that line.
- Tip: document as you write, not at the end. Writing the docstring before the body forces you to clarify what the function does, and sometimes reveals that it does two things and should be split.
Exercises
Exercise 1: Comments that add value
This fragment from Alba Studio's monthly report is full of useless comments and is missing the only one that matters. Rewrite it leaving only valuable comments, and add a docstring and type annotations.
def cost(days, rate):
total = days * rate # multiply days by rate
total = total * 1.21 # multiply by 1.21
if days > 20: # if the days are more than 20
total = total * 0.9 # multiply by 0.9
return total # return the totalExercise 2: Docstring and annotations
Write the Google-style docstring and the full type annotations for this function, including the Raises section:
def load_team(path):
with open(path, encoding="utf-8") as f:
return [line.strip().lower() for line in f if line.strip()]Exercise 3: Versioning and CHANGELOG
Marta wants a separate program, reports, that reads EasyTask's tasks.json. Write the first two sections of its README.md (what it is and requirements) and decide which version number EasyTask deserves after each of these three changes, justifying it: (a) a progress calculation that returned 101 % is fixed; (b) CSV export is added; (c) Task stops accepting the days parameter and starts requiring a due date.
Solutions
Solution 1.
VAT, LONG_DISCOUNT, LONG_PROJECT_DAYS = 1.21, 0.9, 20
def cost(days: int, rate: float) -> float:
"""Return the cost of a job with VAT and a volume discount.
Args:
days: Estimated working days.
rate: Price per day in euros, before tax.
Returns:
Final amount in euros, VAT included.
"""
total = days * rate * VAT
# Alba Studio applies a 10% discount from 20 days onwards:
# agreed with Marta in the 2026 rate review.
if days > LONG_PROJECT_DAYS:
total *= LONG_DISCOUNT
return round(total, 2)The five comments that repeated the code are gone and the only one with real information has appeared: where the discount comes from. Notice too that the numbers 1.21, 0.9 and 20 have become named constants, which document themselves and save you from having to explain them (we will come back to this technique in 08-05). The docstring tells what the signature does not: that the rate is before tax and that the result includes it.
Solution 2.
def load_team(path: str) -> list[str]:
"""Read the team names from a text file.
Every non-empty line is a name, returned trimmed and in lowercase.
Args:
path: Path to the text file, encoded in UTF-8.
Returns:
List of normalised names, in file order.
Raises:
FileNotFoundError: If the file does not exist.
UnicodeDecodeError: If the file is not UTF-8.
"""
with open(path, encoding="utf-8") as f:
return [line.strip().lower() for line in f if line.strip()]The Raises section is the one most often forgotten and often the most useful: it warns the caller that this function can fail and what they will have to protect themselves from. How those exceptions are caught is precisely the subject of the next lesson.
Solution 3.
# Alba Reports Generates a monthly summary of tasks by assignee from the `tasks.json` produced by EasyTask, for the team review on the first Monday of each month. ## Requirements - Python 3.10 or later and a `tasks.json` from EasyTask 0.18 or later.
The first sentence is the most important in the whole file: it says what the program is and who it is for, and some readers will not read anything else. As for the versions: (a) is a patch (0.18 → 0.18.1), because it fixes a bug without changing how the program is used; (b) is a minor version (0.18 → 0.19), new functionality that breaks nothing that came before; and (c) is a major change, because all the code that created tasks with days stops working: if the project were already at 1.0, it would go to 2.0.
Conclusion
Documenting means writing what the code cannot say on its own, and it has three tools with three different audiences. Comments (#) explain the why and not the what: business rules, rejected decisions, warnings and traps found the hard way; anything that repeats the line next to it is surplus, and every comment that is not kept up to date ends up lying, which is worse than being missing. The markers TODO, FIXME, HACK and NOTE record what is pending at the exact spot where it is. Docstrings, with triple quotes and an imperative first line as per PEP 257, live in modules, packages, classes and functions, are consulted with help() and __doc__, and are written in one of the three usual styles — Google, NumPy or reStructuredText; here we chose Google, and the decisive thing is not to mix them. Type annotations (str, list[str], dict[str, int], Task | None, -> None) are documentation your editor also understands, with the clear warning that Python does not check them at run time: that is what mypy is for. The README.md is the project's front door and answers, in order, what it is, what it needs, how it is installed, how it is used, how it is organised and under what licence; the CHANGELOG.md tells what changed in each version, and semantic versioning MAJOR.MINOR.PATCH explains where the numbers this course has used from the start come from. With python -m pydoc -b all of it becomes browsable documentation without installing anything, and Sphinx or MkDocs would do the same on a grand scale.
EasyTask is now v0.18: same behaviour, but with a module docstring in every file, Task and Agenda fully documented, type annotations on the public functions and a README.md that lets anyone start the program in five minutes. The project is understandable. What it still does not do is hold up: if tasks.json is corrupted or someone types "three" where the program expects a number of days, the result is still a traceback in the user's face and a lost session. That is the promise the course has been carrying since 02-04 and since 05-05, and its turn comes in Debugging and error handling: reading a traceback without fear, catching with try/except only what should be caught, raising your own exceptions such as InvalidTask, recording what happens with logging instead of print, and debugging with breakpoints instead of guessing.
Fundamentals of Programming
Module 1: Introduction to Programming
- What is programming?
- History of programming
- Programming languages
- Development environments
- From problem to algorithm
Module 2: Core Concepts
- Variables and data types
- Operators and expressions
- Input and output
- Type conversion and data validation
Module 3: Control Structures
Module 4: Functions and Procedures
- Defining and using functions
- Parameters and return values
- Variable scope
- Breaking a program down into functions
- Functions as values: lambda and higher order
Module 5: Data Structures
- Lists and arrays
- Strings
- Dictionaries and sets
- Tuples and nested structures
- Saving data to files: text, CSV and JSON
Module 6: Basic Algorithms
Module 7: Objects and Code Organisation
- From data to objects: classes and instances
- Attributes, methods and the constructor
- Collections of objects
- Modules, packages and imports
Module 8: Good Practices and Tools
- Documentation and comments
- Debugging and error handling
- Version control
- Automated testing
- Style, readability and refactoring
