Skip to content

Where to go next

Goal: know what this course left out, where to read about it, and how to keep going on your own.

Why this matters

You have finished the course. You planned and built a real program, and in the last module you learned the last big piece of the language's shape. The most useful thing a course can do at this point is be honest about what it did not teach, so that when you meet those things in other people's code you recognise them as not learned yet rather than too hard for me. After that, the only way forward is to build something, and the second half of this lesson is about how to do that without a course holding your hand.

What this course left out

Each of these has a short taster below. None of them was needed for anything you built, and each one is a thing you will see in the first hour of reading other people's Python. Read the tasters now, and come back when you meet one for real.

Tuples

Module 5 said a function returning two values bundles them into a tuple, and left it there. Here is the rest:

tuples.py
# A tuple is a list that cannot change. Round brackets instead of square ones.
point = (3, 4)
print(point[0])
print(len(point))

# You have made tuples already: returning two values from a function makes one.
def lowest_and_highest(numbers):
    return min(numbers), max(numbers)

result = lowest_and_highest([82, 95, 77, 60])
print(result)

# And "catching two values" is unpacking a tuple, which works on any tuple.
low, high = result
print(f"{low} to {high}")

# Trying to change one is an error. That is the point: a tuple is a promise that the values stay together.
try:
    point[0] = 5
except TypeError as error:
    print(f"TypeError: {error}")
3
2
(60, 95)
60 to 95
TypeError: 'tuple' object does not support item assignment

A tuple is a list that cannot change, written with round brackets. Indexing, len() and loops all work as they do on a list. What you cannot do is append to it or assign into it. That makes tuples the natural choice for a few values that belong together and should stay together: a point, a pair of low and high, a date as year, month and day. low, high = result is unpacking, and it is what has been happening every time you caught two values from a function.

Counting with enumerate()

Every numbered table in this course kept a number variable and added one to it at the end of the loop. Python has a shortcut:

numbered_list.py
tasks = ["Buy milk", "Water the plants", "Finish Module 11"]

# The way this course numbered things: a counter you update yourself.
number = 1
for task in tasks:
    print(f"{number}. {task}")
    number += 1

print()

# enumerate() does the counting for you. start=1 makes it begin at 1 instead of 0.
for number, task in enumerate(tasks, start=1):
    print(f"{number}. {task}")
1. Buy milk
2. Water the plants
3. Finish Module 11

1. Buy milk
2. Water the plants
3. Finish Module 11

enumerate(tasks, start=1) hands the loop two things each time, the number and the item, and the loop line unpacks them, exactly as low, high = ... did above. Without start=1 the numbers begin at 0. The version with the counter is not wrong. This one is shorter and cannot forget the number += 1.

Format specs: decimals, money, percentages

The part after the colon in an f-string, which Module 9 used for column widths, is called a format spec, and widths are the least of what it can do:

format_specs.py
price = 2.5
share = 38.5 / 3

# The part after the colon in an f-string is a format spec. You have used widths since Module 9.
# .2f means: a number with exactly two digits after the decimal point.
print(f"{price:.2f}")
print(f"{share}")
print(f"{share:.2f}")

# Width and decimals combine. This is how money columns line up.
print(f"{'Item':<10}{'Price':>8}")
print(f"{'nails':<10}{price:>8.2f}")
print(f"{'hammers':<10}{12.0:>8.2f}")

# Two more that come up often: thousands separators, and percentages.
print(f"{1234567:,}")
print(f"{0.257:.1%}")
2.50
12.833333333333334
12.83
Item         Price
nails         2.50
hammers      12.00
1,234,567
25.7%

.2f is the one you will use most: a number with exactly two digits after the point, which is what every price needs. It combines with a width, so >8.2f is right-aligned in eight characters, two decimals, and money columns line up. , puts in thousands separators and .1% turns a fraction into a percentage with one decimal.

List comprehensions

Building one list from another has been an empty list, a loop and an append(). There is a one-line form:

comprehensions.py
scores = [82, 95, 77, 60]

# The way this course built one list from another: an empty list, a loop, append.
doubled = []
for score in scores:
    doubled.append(score * 2)
print(doubled)

# A list comprehension says the same thing in one line.
# Read it as: "score * 2, for each score in scores".
doubled = [score * 2 for score in scores]
print(doubled)

# It can filter as well. Read it as: "each score in scores, if the score is 70 or more".
passed = [score for score in scores if score >= 70]
print(passed)

# When it stops fitting on one line, go back to the loop. Both are correct.
[164, 190, 154, 120]
[164, 190, 154, 120]
[82, 95, 77]

Read [score * 2 for score in scores] out loud as score times two, for each score in scores, and it says exactly what the loop said. The if at the end filters. Comprehensions are everywhere in Python, and you should be able to read them. Writing them is a matter of taste: when one does not fit comfortably on a line, the loop is better, and nobody will think less of you for using it.

Dataclasses: the Task class in five lines

Look at __init__ in task.py. It takes three values and stores three values, and half of every class you will ever write starts the same way. Python can write that part for you:

dataclass_task.py
from dataclasses import dataclass
from datetime import date

# @dataclass writes __init__, a readable print form and == for you, from the attribute list.
@dataclass
class Task:
    title: str
    due: date
    priority: int
    done: bool = False

milk = Task("Buy milk", date(2026, 9, 24), 2)
print(milk)
print(milk.title)

milk.done = True
print(milk == Task("Buy milk", date(2026, 9, 24), 2, True))
Task(title='Buy milk', due=datetime.date(2026, 9, 24), priority=2, done=False)
Buy milk
True

@dataclass on the line above the class tells Python to read the attribute list and generate __init__ from it, and it throws in two more things: a readable print() form, and == that compares attribute by attribute, which is exactly what the storage test in Module 10 had to work around. The : str and : date parts are type hints, a note saying what kind of value each attribute holds. Python does not enforce them, but editors and other tools read them, and you will see them on functions too: def add_days(when: date, days: int) -> date:. Methods go in a dataclass exactly as before, and the last exercise does that.

Smaller things you will bump into

  • raise ValueError("...") makes an error happen on purpose, so that a function can refuse bad input the way int() does, and a caller can catch it with try.
  • pass is a line that does nothing, for when Python requires a body and you have nothing to put there yet.
  • 0 < x < 10 is allowed, and means what it looks like.
  • A set is a collection with no duplicates and no order, written {1, 2, 3}. set(words) is the quick way to find the different words in a list.
  • lambda writes a tiny function in one expression. tasks.sort(key=lambda task: task.due) is the sort from Module 9 without a separate due_date() function.

Installing other people's code

Everything you imported came with Python. There is far more that does not, and the tool for installing it is pip. Before using it, make a virtual environment: a private copy of Python for one project, so that what you install for one program cannot break another. This is the setup professionals use for every project, and it is three commands.

cd my-project
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install rich
cd my-project
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install rich

The second line creates the environment in a folder called .venv, the third switches your terminal to it, and you will see (.venv) at the start of the prompt while it is active. Activate it again every time you open a new terminal for that project. The last line installs a package, and python -m pip rather than plain pip makes sure it lands in the Python you are actually running.

Three packages worth trying first, each of which does something this course could not:

  • rich prints colour, tables and progress bars in the terminal. The colours item from the Module 9 wish list is from rich import print.
  • requests fetches web pages and web data. A weather report in your to-do app is a few lines with it.
  • matplotlib draws charts. The expense tracker in the ideas below wants one.

Each has documentation with a getting-started page, and reading it is the skill the next section is about. One warning carries over from Module 8: never name your own file after a package. A rich.py in your project folder hides the real one.

Project ideas

The to-do app is a template. Every idea here has the same shape, a class, a storage module and a menu, and each adds one thing you have not done before. Pick the one you would actually use, because a program you use is a program you will keep improving.

Project Same as the to-do app New
Expense tracker An Expense class, a file, a menu Totals per month and per category, and a chart
Flashcards A file of question|answer lines random.shuffle(), keeping score, asking the ones you got wrong again
Habit tracker One line per day per habit Counting streaks with date arithmetic
Quiz from a file Loading questions, a loop, a score Multiple choice, a time limit with time
File organiser A loop and if on the file name The os and shutil modules, and being careful with other people's files
Text adventure Rooms as objects, a while loop, if on the command Nothing at all. It is only bigger, and that is the lesson

Whatever you pick, do it the way Module 9 did: write the plan first, with every function's docstring and no code. Build the storage module and test it before the menu exists. Then build the menu one option at a time, running the program after each. The first exercise below is that plan.

How to read the documentation

The official documentation is at docs.python.org, and the part you want almost every time is the Library Reference, which has a page per module. dir() and help() from Module 8 show the same information offline, so whichever is closer to hand is fine.

The one thing that makes the docs hard at first is the way a function is written, called its signature. Here is split() from the string page:

str.split(sep=None, maxsplit=-1)

You have called split() with no arguments and with one. The signature says why both work: sep=None is a parameter with a default value, which is Module 5's default values exactly. Any parameter shown with = is optional, and the value after = is what you get if you leave it out. maxsplit=-1 means no limit, and the paragraph under the signature says so. That is the pattern for reading any entry: signature first, then the first paragraph, then the examples. The rest of the page is there for when you need it.

How to ask for help

Sooner or later you will be stuck in a way the error message does not fix. When you ask someone, the shape of the question decides how good the answer is:

  1. The smallest program that shows the problem. Not your whole app. Cut it down until removing anything more makes the problem disappear. Very often the problem disappears while you are cutting, and you have your answer.
  2. The exact error, the whole traceback, copied, not retyped and not described. It says something about a key is not enough. KeyError: 'title' on line 34 of todo.py is.
  3. What you expected, and what happened instead. One sentence each.
  4. What you already tried.

Good places to ask are the Python Discord, which has help channels for beginners, and r/learnpython, which exists for exactly these questions. Stack Overflow has an answer to almost everything already, so search it first, and ask there only when you have a precise question that is not already answered. Wherever you ask, the four items above are what people will want, and writing them down is often enough to solve it yourself.

Try it

Make a folder, create and activate a virtual environment in it, and install rich as shown above. Run python -m rich to see what it can do. Then copy your to-do app into the folder, add from rich import print at the top of todo.py, and run it. Nothing else changes, and the output is in colour. Look up rich.table in its documentation and see how far you get replacing show_tasks() with it. That is the whole job from here on: find the tool, read its page, try it in your own program.

Common mistakes

ModuleNotFoundError: No module named 'rich', right after installing it

It was installed into a different Python than the one running your program, usually because pip and python on your computer point at different installations. Always install with python -m pip install, using the same python you run programs with, and inside your activated environment.

'pip' is not recognized as an internal or external command

Use python -m pip instead of pip. It is the same tool, reached through Python, and it always works.

The (.venv) at the prompt has gone, and imports fail again

A new terminal starts outside the environment. Run the activate command again. If you use VS Code, it can activate it for you: search its settings for python interpreter and choose the one in .venv.

SyntaxError: invalid syntax, on a line that starts with >>>

You copied an example from the documentation including the >>> prompt. Those three characters show that the line was typed at the Python prompt. Leave them out.

ModuleNotFoundError: No module named 'requests', and the file I am writing is called requests.py

Module 8's rule: your file is hiding the real package. Rename yours.

I have read three tutorials and still cannot start

That is not a Python problem. Write the plan for the first exercise below, with docstrings and no code, and then write load_...(). Nobody starts by knowing how the whole program goes. They start with one function.

Exercises

  1. Plan a project. Pick one idea from the table, or your own, and write its plan the way Module 9 lesson 1 did: a docstring describing the data and the file format, the list of files, and every function with a docstring and no body. Decide what is not now. Do not write any code yet.
  2. Number the table. Rewrite show_tasks() in the to-do app with enumerate(), and remove the counter.
  3. Money. Take the inventory from the Module 10 exercises and make it print a table with the price and value in two decimals, lined up in columns, with a total at the bottom.
  4. Task in five lines. Rewrite the app's Task as a dataclass, keeping every method. Put it in task.py, run the app to check nothing changed, and then change the storage module's round-trip test to compare the two lists with ==, which now works.
Solution 1
"""The plan for an expense tracker.

An expense is an Expense object: when (a date), amount (a float), category (str) and note (str).
The file has one expense per line: YYYY-MM-DD|amount|category|note

Files:
    expense.py          - the Expense class, to_line(), expense_from_line()
    expense_storage.py  - load_expenses, save_expenses
    expenses.py         - the menu and everything the user sees
    helpers.py          - ask_for_float, ask_for_date, ask_yes_no, read_lines

Not now: budgets per category, charts, several currencies, editing an expense.
"""

# ---- expense.py ----

class Expense:
    """One thing that was paid for: when, how much, what kind, and a note."""

    def to_line(self):
        """Return the expense as one line of text: YYYY-MM-DD|amount|category|note."""

    def is_in_month(self, year, month):
        """Return True if the expense happened in that month of that year."""

def expense_from_line(line):
    """Return the Expense described by one line of the file. A malformed line causes a ValueError."""

# ---- expense_storage.py ----

def load_expenses():
    """Return the saved expenses as a list, skipping bad lines. Empty list if there is no file."""

def save_expenses(expenses):
    """Write every expense to the file, replacing what was there."""

# ---- expenses.py ----

def show_expenses(expenses):
    """Print the expenses as a table, newest first, with a total at the bottom."""

def add_expense(expenses):
    """Ask for the details of a new expense and add it to the list."""

def show_month(expenses, today):
    """Print this month's expenses, and the total for each category."""

def delete_expense(expenses):
    """Ask which expense to delete, confirm, and remove it."""

def main():
    """Load the expenses, run the menu until the user quits, saving after every change."""

Yours will differ, and the plan for an expense tracker is only one possible answer. What matters is that every function has one job that its docstring can state, that the file format is decided before any code, and that the not now list exists. The plan took twenty minutes. It will save hours.

Solution 2
"""show_tasks() from the app, numbered with enumerate(). Only the loop changed.

A small stand-in Task is defined here so the file runs on its own. In the app, import the real one.
"""

from datetime import date

class Task:
    def __init__(self, title, due, priority):
        self.title = title
        self.due = due
        self.priority = priority
        self.done = False

def show_tasks(tasks):
    """Print the tasks as a numbered table."""
    if len(tasks) == 0:
        print("Nothing to do!")
        return
    print(f"{'#':>2}  {'':<4}{'Title':<28}{'Due':<12}{'Pri':>3}")
    for number, task in enumerate(tasks, start=1):
        if task.done:
            mark = "[x]"
        else:
            mark = "[ ]"
        print(f"{number:>2}  {mark:<4}{task.title:<28}{str(task.due):<12}{task.priority:>3}")

tasks = [
    Task("Call the dentist", date(2026, 9, 18), 3),
    Task("Buy milk", date(2026, 9, 24), 2),
    Task("Finish Module 11", date(2026, 10, 3), 1),
]
tasks[0].done = True
show_tasks(tasks)

Two lines gone, one changed. The stand-in Task at the top is only there so the file runs on its own.

Solution 3
class Item:
    def __init__(self, name, quantity, price):
        self.name = name
        self.quantity = quantity
        self.price = price

    def __str__(self):
        return f"{self.name}: {self.quantity} at {self.price:.2f} each"

    def total_value(self):
        """Return what the stock of this item is worth."""
        return self.quantity * self.price

def stock_value(items):
    """Return the value of every item added together."""
    total = 0
    for item in items:
        total += item.total_value()
    return total

items = [
    Item("nails", 4, 2.5),
    Item("screws", 6, 0.75),
    Item("hammers", 2, 12.0),
]

print(f"{'Item':<10}{'Qty':>5}{'Each':>8}{'Value':>9}")
for item in items:
    print(f"{item.name:<10}{item.quantity:>5}{item.price:>8.2f}{item.total_value():>9.2f}")
print(f"{'Total':<23}{stock_value(items):>9.2f}")

print()
for item in items:
    print(item)

>8.2f and >9.2f do all the work. The total line pads its label to the width of the first three columns together, so the number lands under the values.

Solution 4
"""The app's Task as a dataclass. The methods are unchanged; __init__ and == come for free."""

from dataclasses import dataclass
from datetime import date

@dataclass
class Task:
    title: str
    due: date
    priority: int
    done: bool = False

    def __str__(self):
        if self.done:
            mark = "[x]"
        else:
            mark = "[ ]"
        return f"{mark} {self.title} (due {self.due}, priority {self.priority})"

    def finish(self):
        """Mark the task as done."""
        self.done = True

    def is_overdue(self, today):
        """Return True if the task is not done and its due date has passed."""
        return not self.done and self.due < today

    def describe_due(self, today):
        """Return a short phrase saying when the task is due, or an empty string if it is done."""
        if self.done:
            return ""
        days = (self.due - today).days
        if days == 0:
            return "today"
        elif days == 1:
            return "tomorrow"
        elif days > 1:
            return f"in {days} days"
        elif days == -1:
            return "OVERDUE by 1 day"
        else:
            return f"OVERDUE by {-days} days"

    def to_line(self):
        """Return the task as one line of text: title|YYYY-MM-DD|priority|yes or no."""
        if self.done:
            done = "yes"
        else:
            done = "no"
        return f"{self.title}|{self.due}|{self.priority}|{done}"

def task_from_line(line):
    """Return the Task described by one line of the file. A malformed line causes a ValueError."""
    title, due_text, priority_text, done_text = line.split("|")
    task = Task(title, date.fromisoformat(due_text), int(priority_text))
    if done_text == "yes":
        task.finish()
    return task

if __name__ == "__main__":
    print("Testing the dataclass Task")
    original = Task("Book flights, hotel and car", date(2026, 11, 1), 1)
    original.finish()
    back = task_from_line(original.to_line())
    print(back)
    print(f"Same as the original, using ==: {back == original}")

Everything below the attribute list is the Module 10 class, untouched. __str__ is still yours, because the print form a dataclass generates is for programmers, not users, and the app's print(task) should stay readable. With == generated, the storage test can go back to loaded == original.

Summary

  • Tuples, enumerate(), format specs, comprehensions and dataclasses are the things you will meet first in other people's code. None was needed here, and each is a short read when you get to it.
  • A virtual environment per project, and python -m pip install inside it, is how packages are installed.
  • Read a documentation entry as signature, first paragraph, examples. A parameter with = is optional.
  • A good question is the smallest program, the exact error, what you expected, and what you tried.
  • The next project has the shape of the to-do app: plan, storage module with a test, then the menu.

The end of the course

Eleven modules ago, you had never written a line of code. Now you can read most Python you will come across, write programs that keep their data and survive bad input, and, more importantly, you know how to find out what you do not know. That last part is what programming actually is. Nobody has it all in their head. They have a way of working, and now so do you.

Build something. Make it for yourself, make it small, and make it work. Then make it better.

Back to the course home