A field guide for first-time coders

Learn Python.
Make it useful.

Five small, real-world projects. Every command explained. No experience, paid API, or mystery step required.

  • 5working projects
  • 0skills assumed
  • $0APIs required
your first field note

$ python3 expense_summary.py

Monthly expense summary

Skipped 2 messy row(s)

Wrote output/monthly_summary.csv

2026-01 · Food (4) $125.41

Grand total $5603.10

You’ll understand
every line of this.

One path. Five useful things.

Work top to bottom. Each project introduces only a few new ideas and reuses what you already learned.

Before the first line of code

Build your workbench.

About 20 minutes, and you only do it once. Read the six words below first, then work through the six setup steps that follow.

Words you’ll keep seeing

Read these once so the instructions make sense. There is nothing to install or type in this part.

Language

Python

The language you’ll write and the program that runs your instructions.

App

Terminal

A text-based way to give your computer precise commands.

Tool

Git

A tool that records changes to files, like save points for a project.

Website

GitHub

A website that stores Git repositories so people can copy and collaborate on them.

Folder

Repository

A project folder whose files and history Git tracks; “repo” is short for repository.

Packages

venv + pip

A venv isolates a project’s packages; pip installs those packages into it.

The six setup steps

Now you start doing things. Take them in order, Setup step 1 through Setup step 6; each one ends with something you can check before moving on.

  1. Setup step 1 of 6 · Account · 3 minutes

    One time

    Create a GitHub account

    Open github.com/signup in your browser. Enter your email, create a password and username, then complete email verification. A free account is enough.

    Checkpoint

    You can sign in at github.com and see your dashboard.

  2. Setup step 2 of 6 · Install · 5–10 minutes

    Choose your OS

    Install Python and Git

    Python runs your code. Git downloads the project and tracks your changes. Follow only the panel for your computer.

    macOS

    1. Download the latest Python 3 installer from python.org/downloads/macos and run the .pkg.
    2. Open Terminal from Applications → Utilities.
    3. Type git --version. If macOS asks to install developer tools, choose Install.

    Windows

    1. Download Python from python.org/downloads/windows. On the first installer screen, check Add python.exe to PATH, then install.
    2. Download and install Git from git-scm.com/download/win. Keep the suggested options.
    3. Open PowerShell from the Start menu.

    In Terminal or PowerShell, check both installs:

    i

    Choose your computer in each command box. When macOS and Windows differ, a toggle appears beside Copy. Your choice is remembered and synchronized across the whole course.

    Terminal / PowerShell
    python3 --version
    git --version
    i

    Windows note The Windows side of each command toggle uses python; the macOS side uses python3. You do not need to translate it yourself.

    You should see
    Python 3.12.x
    git version 2.x.x

    Your exact numbers may be newer.

  3. Setup step 3 of 6 · Download · 2 minutes

    Terminal

    Clone the repository

    To clone means to download a Git repository, including its project files and history. First move to your Desktop, then clone.

    i

    This is the real course repository. Copy all three lines below exactly. Git downloads every lesson, generator, test, and supporting file.

    Terminal / PowerShell
    cd Desktop
    git clone https://github.com/cassidythilton/python-from-zero.git
    cd python-from-zero
    You should see
    Cloning into 'python-from-zero'...
    Receiving objects: 100% ... done.

    Your prompt now ends inside the repository folder. That folder is the repo root: it contains index.html, README.md, and tutorials/.

  4. Setup step 4 of 6 · Editor · 2 minutes

    Your choice

    Install an editor, then open the project

    An editor is the app where you read and change code. Choose either option below. Both are free for this course and both run the same Python files.

    Cursor

    1. Open cursor.com/downloads.
    2. Choose the download for your computer: macOS or Windows.
    3. Open the downloaded installer and follow its prompts.
    4. Start Cursor. You do not need a paid plan for these tutorials.

    Visual Studio Code

    1. Open code.visualstudio.com/Download.
    2. Choose the download for your computer: macOS or Windows.
    3. Open the downloaded installer and follow its prompts.
    4. Start Visual Studio Code.

    In the editor you chose, select File → Open Folder…, choose the cloned python-from-zero folder on your Desktop, and select Open.

    If the code or cursor terminal command is installed, you can instead type one of these:

    Terminal / PowerShell · choose one
    cursor .
    # or
    code .
    Checkpoint

    The editor’s file panel shows index.html and a tutorials folder holding 01-expense-summary through 05-inbox-agent.

  5. Setup step 5 of 6 · Isolate · 3 minutes

    Terminal

    Create and activate a virtual environment

    A virtual environment, or venv, is a private box for this project’s Python packages. It prevents one project’s packages from interfering with another’s.

    In your editor, open Terminal → New Terminal. Make sure the prompt is at the repo root, then create the venv:

    All systems · run once at the repo root
    python3 -m venv .venv

    Now activate it each time you return to this course. Use the macOS/Windows toggle in the command box:

    Activate the course environment
    source .venv/bin/activate
    You should see
    (.venv) ...python-from-zero

    The exact final symbol varies by computer. The (.venv) at the start means it is active.

    i

    One venv is enough. Modules 1, 2, and 4 need no packages at all. Modules 3 and 5 install one or two packages into whichever venv is active. Their folder READMEs show an optional per-folder .venv instead; either choice works, so long as your prompt shows (.venv) before you run pip.

    i

    PowerShell blocked the script? Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, answer Y, then activate again.

  6. Setup step 6 of 6 · Final check · 1 minute

    Ready

    Meet pip and confirm your location

    pip is Python’s package installer. A package is reusable code someone else has prepared. You’ll use pip only when a lesson needs a package.

    Terminal / PowerShell · at the repo root
    python3 -m pip --version
    python3 -c "print('Workbench ready!')"
    You should see
    pip ... from .../.venv/...
    Workbench ready!

    If the pip path contains .venv, your private environment is working.

Your learning path

Five projects, one growing toolkit.

Each field assignment takes roughly 45–90 minutes. The trail gets more interesting—not more mysterious. Every module tells you which folder your terminal should be in before the first command.

Module01of 05

Personal finance · 45–60 min

Expense cartographer

Clean a messy expense file, group purchases by month and category, then save a summary someone could actually use.

LearnVariables, types, if, functions, files, CSV

MimicsA finance or operations task

Raw rows become a useful answer.

A CSV (comma-separated values) file is a plain-text table. Real CSV data is often inconsistent: extra spaces, different date formats, dollar signs, or category names with mixed capitalization. Your program cleans each row, then totals what is left.

data/expenses.csvclean + groupoutput/monthly_summary.csv
  • VariableA named place to keep a value, such as amount = 12.50.
  • TypeThe kind of value: text (str), number (float), or something else.
  • FunctionA named, reusable group of instructions that does one job, like parse_date.
  • if statementA choice: run code only when a condition is true.

Do these steps in order.

  1. 1

    Put your terminal in the project folder

    Open your editor’s terminal and confirm (.venv) appears. Every command in this module assumes your terminal is inside tutorials/01-expense-summary.

    Terminal · from the repo root
    cd tutorials/01-expense-summary
    i

    Prefer to stay at the repo root? Put the folder in front of the file name instead: python3 tutorials/01-expense-summary/generate_data.py. Both ways work, because each script finds its own data/ and output/ folders.

    Inspect: generate_data.py, then expense_summary.py.

  2. 2

    Generate the messy practice file

    The generator writes the same 37 rows every run, so your screen matches this page. Nothing is downloaded.

    Terminal · in 01-expense-summary
    python3 generate_data.py
    You should see
    Wrote 37 expense rows to .../tutorials/01-expense-summary/data/expenses.csv
    Some dates, categories, and amounts are messy on purpose.

    The ... is wherever the project lives on your computer.

    Open data/expenses.csv. Look for 1/8/2026 next to 2026-01-03, $12.50 next to 15.99, and two deliberately broken rows: one with not-a-date and one with a blank amount.

  3. 3

    Read the cleaning functions

    In expense_summary.py, read four functions in this order: parse_date tries each date pattern, parse_amount strips $ and commas before float(), normalize_category maps many spellings onto tidy names through CATEGORY_ALIASES, and clean_row returns one clean dictionary or None when the row is unusable.

    Make one change: in CATEGORY_ALIASES, change "coffee": "Food" to "coffee": "Coffee" and save. After step 4 the January block gains Coffee (1) $ 6.75 and Food drops to Food (3) $ 118.66. Change it back if you want your output to match the sample below.

  4. 4

    Run the summary

    Terminal · in 01-expense-summary
    python3 expense_summary.py
    You should see
    Monthly expense summary
    =======================
    2026-01
      Entertainment (2) $   29.99
      Food (4)         $  125.41
      Housing (1)      $ 1450.00
      Transport (2)    $   60.70
      Utilities (2)    $  172.39
      Month total      $ 1838.49
    
    ... (2026-02 and 2026-03 print the same way)
    
    Grand total: $5603.10
    Skipped 2 messy row(s) that could not be cleaned.
    Wrote .../tutorials/01-expense-summary/output/monthly_summary.csv

    The script already prints the grand total, and the two skipped rows are the broken ones you found in step 2. Open output/monthly_summary.csv: csv.DictReader read the named input columns and csv.DictWriter wrote month,category,total,transaction_count.

  5. 5

    Optional: run the folder’s tests

    A test is a small program that checks your program still behaves.

    Terminal · in 01-expense-summary
    python3 -m unittest discover -s tests -v
    You should see
    Ran 15 tests in 0.0..s
    
    OK

    From the repo root the same checks run with python3 -m unittest discover -s tutorials/01-expense-summary/tests.

Put the grand total in the file, too.

The terminal prints Grand total, but output/monthly_summary.csv does not contain it. In write_summary_csv, add one final row after the loop with month set to TOTAL, an empty category, and the sum of every total. Keep the four column names exactly as they are—the tests check that header.

You’re done when…

  • data/expenses.csv exists with 37 rows.
  • The terminal prints Grand total: $5603.10 and Skipped 2 messy row(s).
  • output/monthly_summary.csv opens as a table of month,category,total,transaction_count.
  • You can explain why "$12.50" has to become the number 12.5.
Troubleshooting module 1

Could not find .../data/expenses.csv: run python3 generate_data.py first. can't open file 'generate_data.py': your terminal is not in tutorials/01-expense-summary—either cd into it or use the longer repo-root form. python: command not found: use python3; on Windows use python. Numbers look different: you edited the aliases or the CSV. Run python3 generate_data.py again to restore the original rows.

Module02of 05

Customer support · 50–70 min

Ticket signal finder

Give incoming support tickets useful tags by scoring keywords, then count what the team should handle.

LearnLists, dictionaries, modules, loops, pipelines

MimicsA customer-operations sorter

A simple pipeline that finds meaning.

A support team needs to route many messages. Your program loads 12 generated tickets, counts how many keywords from each tag list appear in the subject and body, keeps the highest-scoring tag, and prints a count-by-tag table before saving the tagged list.

data/tickets.jsonscore keywordsoutput/tagged_tickets.json
  • ListAn ordered collection, such as the queue of tickets in square brackets.
  • DictionaryValues stored under named keys, such as ticket["subject"].
  • ModuleA Python file whose functions another file can import; main.py imports tagger.py.
  • PipelineSmall stages where one stage’s output becomes the next stage’s input.

Build the tagger’s mental model.

  1. 1

    Move into module 2

    Every command here assumes your terminal is inside tutorials/02-ticket-tagger. Pick the line that matches where you are now.

    Coming from module 1
    cd ../02-ticket-tagger
    From the repo root
    cd tutorials/02-ticket-tagger

    This module needs no packages—json ships with Python. Now create the ticket file:

    Terminal · in 02-ticket-tagger
    python3 generate_data.py
    You should see
    Wrote 12 tickets to .../tutorials/02-ticket-tagger/data/tickets.json
  2. 2

    Inspect the data shape

    Open data/tickets.json. The outer square brackets are a list; each object in curly braces is a dictionary. Find the id, subject, body, and priority keys.

    Then inspect tagger.py and main.py. Notice that main.py imports the tagging logic instead of repeating it.

  3. 3

    Trace one score

    In tagger.py, KEYWORD_LISTS maps five tags—login, billing, shipping, bug, feature—to keywords. combine_text lowercases the subject plus body, score_tag counts matches, and choose_tag keeps the best score. Two rules matter: a tie goes to whichever tag comes first in TAG_ORDER, and a ticket with no matches falls back to DEFAULT_TAG, which is general.

  4. 4

    Run the complete pipeline

    Terminal · in 02-ticket-tagger
    python3 main.py
    You should see
    Tagged tickets
    --------------
    1. [login] Cannot login to my account
    2. [billing] Wrong charge on my invoice
    3. [shipping] Where is my package?
    4. [bug] App crash on save
    5. [feature] Feature request: export to PDF
    6. [general] Office hours for the downtown shop
    7. [login] Sign in button does nothing
    8. [billing] I need a refund
    9. [shipping] Shipment arrived damaged
    10. [bug] Exception after clicking Help
    11. [feature] Would like bulk edit
    12. [general] How do I change my display name?
    
    Count by tag
    ------------
    login: 2
    billing: 2
    shipping: 2
    bug: 2
    feature: 2
    general: 2
    total: 12
    
    Wrote 12 tagged tickets to .../tutorials/02-ticket-tagger/output/tagged_tickets.json

    Because the generated tickets never change, these tags and counts should match exactly. Open output/tagged_tickets.json: every ticket dictionary now has an extra "tag" key.

  5. 5

    Change a keyword and watch a tag move

    In tagger.py, add "display name" to the login keyword list, save, and run python3 main.py again.

    You should see
    12. [login] How do I change my display name?
    ...
    login: 3
    general: 1

    Then undo it. Remove "display name" again: the folder’s tests expect the original keyword lists and the original 2-per-tag counts.

  6. 6

    Optional: run the folder’s tests

    Terminal · in 02-ticket-tagger
    python3 -m unittest discover -s tests -v
    You should see
    Ran 15 tests in 0.0..s
    
    OK

Give urgent tickets a flag.

In main.py, inside print_ticket_list, print URGENT next to any ticket whose priority equals "high". Use an if statement and the ticket["priority"] value you saw in step 2.

You’re done when…

  • data/tickets.json holds 12 tickets.
  • Every printed ticket has exactly one tag.
  • The count table shows 2 per tag and total: 12.
  • output/tagged_tickets.json exists and each dictionary has a "tag" key.
Troubleshooting module 2

Could not find .../data/tickets.json: run python3 generate_data.py first. ModuleNotFoundError: No module named 'tagger': run the command from inside 02-ticket-tagger; the repo-root form python3 tutorials/02-ticket-tagger/main.py also works, because Python adds the script’s own folder to its search path. Counts don’t match: restore the original keyword lists and rerun the generator. JSON error: regenerate data/tickets.json—a hand edit probably removed a comma or quote.

Module03of 05

Internal tools · 60–90 min

Inventory lookout

Build a tiny web app that lists stock, searches it, and adjusts quantities—running only on your own computer.

LearnFlask, handlers, requests, responses, forms, templates

MimicsAn internal inventory tool

Python meets the browser.

Flask is a small Python package for web applications. A browser sends an HTTP request; a Flask function handles it and returns a response, usually rendered from an HTML template. There is no database here: the items live in one JSON file.

browser requestFlask routedata/inventory.jsonHTML template
  • RouteA URL path, such as /search, connected to a Python function.
  • HandlerThe function that runs when a matching request arrives.
  • Query stringThe part after ? in a web address, such as ?q=coffee.
  • TemplateAn HTML file with blanks Flask fills in with your data.
  • Local serverA program only your computer can reach while it runs.

Start your first local server.

  1. 1

    Move into module 3

    Every command here assumes your terminal is inside tutorials/03-flask-inventory.

    Coming from module 2
    cd ../03-flask-inventory
    From the repo root
    cd tutorials/03-flask-inventory
  2. 2

    Install Flask

    This is the first module that needs a package. Check that your prompt shows (.venv), then install what requirements.txt lists.

    Terminal · in 03-flask-inventory
    python3 -m pip install -r requirements.txt
    You should see
    Successfully installed Flask-3.x.x ...

    Version numbers and extra package names may differ.

    i

    Two valid setups. Installing into the repo-root .venv from Start here is fine. The folder README instead creates a .venv inside this folder; if you follow that, activate it first and use python in place of python3.

  3. 3

    Generate the inventory file

    Terminal · in 03-flask-inventory
    python3 generate_data.py
    You should see
    Wrote 24 items to data/inventory.json
    First item: OFF-1001 - Ballpoint Pens (12 pack)

    The generator uses a fixed random seed, so the 24 items are identical on every computer.

  4. 4

    Connect files to responsibilities

    Inspect app.py for the three routes, inventory.py for the logic, data/inventory.json for the stock data, and templates/ plus static/style.css for presentation.

    Find the @app.route lines: / lists every item, /search reads request.args, and /adjust accepts a submitted form through request.form. Notice that inventory.py contains no web code at all—that separation is the main idea of this module.

  5. 5

    Run the development server

    Terminal · in 03-flask-inventory
    python3 app.py
    You should see
     * Serving Flask app 'app'
     * Debug mode: on
     * Running on http://127.0.0.1:5000
    Press CTRL+C to quit

    The terminal now looks stuck; it is waiting for requests. Leave it open and visit http://127.0.0.1:5000. 127.0.0.1 always means “this computer,” and 5000 is the port Flask is listening on.

  6. 6

    Send three requests

    The home page shows a line like 24 items in stock list · 5 at or below 10 units above a 24-row table.

    1. Search coffee. The address becomes /search?q=coffee and 2 matching items remain.
    2. Search zzzzz. You get the Nothing matched “zzzzz” message instead of an error.
    3. Search a SKU such as OFF-1001 to see the single matching row.

    Each request adds a new line in the terminal—that is the server logging what the browser asked for.

  7. 7

    Submit the form that changes data

    In any row, type 5 in the last column and press Apply. The browser sends a POST request to /adjust, and a note appears at the top:

    You should see
    OFF-1001 changed from 7 to 12.

    Your starting number may differ if you already adjusted that item.

    The new quantity is written back to data/inventory.json, so it survives a restart. Run python3 generate_data.py again whenever you want the original numbers back.

  8. 8

    Stop the server, then optionally test

    Click the terminal and press Control + C (Control, not Command, on a Mac). Your prompt returns and the page stops loading—that is expected.

    Terminal · in 03-flask-inventory, server stopped
    python3 -m unittest
    You should see
    Ran 18 tests in 0.0..s
    
    OK

    test_app.py uses Flask’s test client: a pretend browser that visits pages and submits forms without you clicking.

Let people search by location.

Searching Back Room finds nothing today, because search_items in inventory.py only looks at the SKU, name, and category. Add item["location"] to the text it searches, restart the server, and search Back Room—you should get the 6 items stored there.

You’re done when…

  • The home page lists 24 items at 127.0.0.1:5000.
  • Searching coffee returns 2 rows.
  • Searching zzzzz shows the “Nothing matched” message.
  • An Apply changes a quantity and the change is still there after a restart.
  • You can describe the trip from request to handler to response.
Troubleshooting module 3

No module named flask: your venv is not active or step 2 was skipped; check for (.venv) and install again. “No inventory file yet” on the page: press Control+C, run python3 generate_data.py, then start the app again. Address already in use / port 5000 busy: an older copy is still running—stop it with Control+C. On macOS, AirPlay Receiver can also hold port 5000 (System Settings → General → AirDrop & Handoff). Browser can’t connect: python3 app.py must keep running while you browse. TemplateNotFound: the files in templates/ must keep the names base.html, index.html, search.html, and _items_table.html.

Module04of 05

Automation · 55–75 min

Report dispatch

Turn a folder of daily JSON exports into one tidy summary that does not fall over when a file is junk.

LearnJSON, try/except, modules, paths, output files

MimicsThe Monday-morning report

Reliable automation expects trouble.

JSON is a plain-text way to write data that people and programs can both read: curly braces hold "name": value pairs, square brackets hold lists. Real exports arrive with missing fields, hand-typed prices, and occasionally a half-written file. This program reports each problem and keeps going.

data/daily/*.jsonload + validatereports/summary.txt
  • try / excepttry means “attempt this”; except means “if it fails, do this instead of crashing.”
  • Missing keyA dictionary does not contain the name the code expected, like "amount".
  • PathA file’s location, such as reports/summary.txt.
  • ModuleOne .py file another can import; the tests here import report_builder.

Make the report—and make it resilient.

  1. 1

    Move into module 4

    Every command here assumes your terminal is inside tutorials/04-json-report. This module needs no packages.

    Coming from module 3
    cd ../04-json-report
    From the repo root
    cd tutorials/04-json-report
  2. 2

    Create the practice data

    Terminal · in 04-json-report
    python3 generate_data.py
    You should see
    Wrote 2024-03-04.json  (6 orders)
    Wrote 2024-03-05.json  (8 orders)
    Wrote 2024-03-06.json  (8 orders)
    Wrote 2024-03-07.json  (6 orders)
    Wrote 2024-03-08.json  (broken on purpose)
    
    Done. 5 files are in .../data/daily
    Next: python3 report_builder.py

    Two files are damaged deliberately: 2024-03-06.json has three orders with missing or badly typed fields, and 2024-03-08.json is not valid JSON at all. You do not have to break anything yourself.

  3. 3

    Read the program in reading order

    Open report_builder.py, then look at data/daily/2024-03-06.json and data/daily/2024-03-08.json.

    All of the logic lives in that one file: clean_amount and clean_quantity rescue a single value, read_day_file opens one file inside a try, summarize_day adds up one day, load_folder loops over every file, build_summary_text writes the words, and main runs them in order. Note which exceptions read_day_file catches by name: json.JSONDecodeError and OSError.

  4. 4

    Build the report

    Terminal · in 04-json-report
    python3 report_builder.py
    You should see
    Reading JSON files from: .../data/daily
      read 2024-03-04.json  ->  6 orders, $54.75
      read 2024-03-05.json  ->  8 orders, $90.40
      read 2024-03-06.json  ->  8 orders, $59.00   (3 problem(s))
      read 2024-03-07.json  ->  6 orders, $71.30
      SKIPPED 2024-03-08.json  ->  not valid JSON (line 7): Unterminated string starting at
    
    Wrote report: .../reports/summary.txt
    Tip: run it again with --send to also write outbox/report_ready.txt

    Nothing crashed. The broken file was skipped and said why, and the report is written on every run.

  5. 5

    Read what you produced

    Open reports/summary.txt. It ends like this:

    You should see
    TOTALS
      Orders:  28
      Items:   53
      Revenue: $275.45
    
    BY DAY
      2024-03-04     6 orders    10 items       $54.75
      ...
    
    PROBLEMS FOUND: 4
      2024-03-06.json: R-302: missing "amount", counted as $0.00
      2024-03-06.json: R-304: could not read amount "6,50", counted as $0.00
      2024-03-06.json: R-305: missing "quantity", counted as 0 items
      2024-03-08.json: SKIPPED - not valid JSON (line 7): Unterminated string starting at

    Three problems came from fields inside a readable file; the fourth is the whole file that could not be parsed. The header above also records Files found: 5 (4 read, 1 skipped).

  6. 6

    Pretend to send it

    Terminal · in 04-json-report
    python3 report_builder.py --send
    You should see
    Wrote report: .../reports/summary.txt
    Wrote mock send: .../outbox/report_ready.txt  (pretend email - nothing left your computer)

    --send is a flag: extra text after the file name that changes what the program does. Without it, outbox/report_ready.txt is never written. No email is ever sent.

  7. 7

    Optional: run the folder’s tests

    Terminal · in 04-json-report
    python3 -m unittest
    You should see
    Ran 16 tests in 0.0..s
    
    OK

    The tests use temporary folders, so they never touch your data/, reports/, or outbox/.

Report the busiest day.

In build_summary_text, find which day summary has the largest revenue and add a Busiest day line under TOTALS. Format the number with the existing money() function instead of writing your own dollar formatting.

You’re done when…

  • data/daily/ holds 5 JSON files.
  • reports/summary.txt exists and says PROBLEMS FOUND: 4.
  • The totals read 28 orders, 53 items, $275.45.
  • The broken file is skipped with a reason instead of crashing the run.
  • outbox/report_ready.txt appears only after you pass --send.
Troubleshooting module 4

That folder does not exist yet.: run python3 generate_data.py first. No such file or directory: 'generate_data.py': you are in the wrong folder—redo step 1. ModuleNotFoundError: No module named 'report_builder': run the tests from inside this folder, or use python3 -m unittest discover -s tutorials/04-json-report from the repo root. SKIPPED 2024-03-08.json: correct—that file is broken on purpose. Numbers don’t match: delete data/ and run the generator again.

Module05of 05

Helpdesk automation · 75–90 min

Inbox trail agent

Train a small model on generated emails, then run a loop that picks tools from a fixed list and logs every decision.

LearnTrain/predict, functions-as-tools, a loop with a stop condition

MimicsA tiny helpdesk triage script

An agent you can see all the way through.

A model is a saved recipe that guesses a label from the words in a message. An agent, here, is a constrained agentic workflow: a loop that predicts a label, picks tools from a fixed list with if/elif, writes a log line, and then stops.

This is deliberately not a chatbot. Scikit-learn trains a tiny text classifier on generated emails, four ordinary Python functions do the work, and nothing calls an LLM, an API, or the internet after you install the packages. It is not autonomous intelligence and it cannot invent new actions.

messageclassifylookup_policydraft_replywrite_logstop
  • TrainShow the model labeled examples so it can store word patterns.
  • PredictAsk the saved model to guess a label for a new message.
  • ToolA plain Python function the agent is allowed to call.
  • Stop conditionA clear rule that ends the loop; here, the inbox list running out.

Train first. Then let the agent act.

  1. 1

    Move into module 5 and install the packages

    Every command here assumes your terminal is inside tutorials/05-inbox-agent.

    Coming from module 4
    cd ../05-inbox-agent
    From the repo root
    cd tutorials/05-inbox-agent
    Terminal · in 05-inbox-agent
    python3 -m pip install -r requirements.txt
    You should see
    Successfully installed ... joblib-... scikit-learn-...

    This install is the only step that uses the internet. Everything afterwards runs offline, with no API keys. The folder README creates a .venv inside this folder instead; the repo-root .venv from Start here works just as well.

  2. 2

    Generate labeled examples and an inbox

    Terminal · in 05-inbox-agent
    python3 generate_data.py
    You should see
    Wrote 80 labeled training messages to .../data/training.json
    Wrote 8 inbox messages to .../data/inbox.json
    Wrote local policies to .../data/policies.json
    Done. Next: python train_model.py

    Compare data/training.json with data/inbox.json: training messages carry a label (password_reset, billing, shipping, or spam), inbox messages do not. That gap is exactly what the model fills. data/policies.json holds the local rules the agent looks up.

  3. 3

    Train and save the model

    Open train_model.py. A Pipeline glues two steps together: TfidfVectorizer turns words into numbers, and LogisticRegression learns which words point at which label. Some rows are held back so the score describes messages the model did not learn from.

    Terminal · in 05-inbox-agent
    python3 train_model.py
    You should see
    Saved model to .../model/inbox_model.joblib
    Validation accuracy: 100% (20 held-out generated emails)
    Read this number carefully: it only describes this practice dataset. It is not a claim that the model works on real customer inboxes.
    Done. Next: python agent.py

    The percentage can shift slightly between library versions. It looks perfect because the practice emails use very different words on purpose—it is not a real-world score.

  4. 4

    Inspect the fixed toolbox

    Open tools.py. Four plain functions are the entire toolbox: classify loads the saved model and predicts, lookup_policy reads the local JSON rules, draft_reply fills a canned template, and write_log appends one line to a log file.

    Then open agent.py and read choose_tools. The predicted label alone picks the branch—there is no confidence score and no threshold in this project. password_reset, billing, and shipping get a drafted reply; spam and anything unrecognised get a policy and a log entry but deliberately no customer-facing draft.

  5. 5

    Run the agent and read its trace

    Terminal · in 05-inbox-agent
    python3 agent.py
    You should see
    Inbox triage agent
    Inbox size: 8. The loop stops after the last message.
    
    --- Message 1: INB-001 ---
    Subject: Need a password reset
    Predicted label: password_reset
    Tools used: classify, lookup_policy, draft_reply, write_log
    Policy: Send the password-reset steps. Never ask for the current password. ...
    Draft reply:
    Hi alex,
    ...
    
    --- Message 4: INB-004 ---
    Subject: Claim your prize now
    Predicted label: spam
    Tools used: classify, lookup_policy, write_log
    Draft reply: (none — do not email the sender)
    
    Stop: every inbox message was processed once. No more tools will run.

    All eight messages print a block like this. Message 4 is the visible proof that the branch changed: spam gets no draft_reply.

  6. 6

    Read the audit log

    Open logs/agent_log.jsonl. A .jsonl file holds one JSON object per line, and each line records the time, message id, predicted label, tools used, policy, and draft. Every run appends 8 more lines, so delete the file when you want a clean log.

    i

    Why a fixed tool list? A short, known menu makes the behavior reviewable and limits what the loop can do. This agent only reads local files and writes a local log.

  7. 7

    Change one template and rerun

    In tools.py, find REPLY_TEMPLATES and add a friendly closing line to one label’s template. Save, run python3 agent.py again, and read the new draft.

    What this proves: the prediction did not change, only the wording. Classifying and writing are separate steps, which is why you can fix the words without retraining.

  8. 8

    Optional: run the folder’s tests

    Terminal · in 05-inbox-agent
    python3 -m unittest discover -s tests -v
    You should see
    Ran 9 tests in 0.0..s
    
    OK

    These tests regenerate data/ and retrain the model, and they check files, tools, routing, and that the loop stops. None of them require a particular accuracy number.

Log who wrote in.

The log records the subject but not the sender. In make_log_record in tools.py, add a "from" field using message.get("from"). Run python3 agent.py again and check that the newest lines of logs/agent_log.jsonl include the address. A good audit trail answers “who, what, and which tools?”

You’re done when…

  • model/inbox_model.joblib exists after training.
  • All 8 inbox messages print a tool trace.
  • Spam messages get a policy and a log line, but no draft reply.
  • The run ends with the Stop: line and logs/agent_log.jsonl gains 8 lines.
  • You can explain model, tool, agent, and stop condition in your own words.
Troubleshooting module 5

No module named sklearn: activate your venv and run the install from step 1 again. Missing .../data/training.json: run python3 generate_data.py before training. Missing .../model/inbox_model.joblib: run python3 train_model.py before python3 agent.py. Accuracy looks “too perfect”: expected—the generated emails are easy on purpose, and the number says nothing about real inboxes. Log keeps growing: each run appends; delete logs/agent_log.jsonl for a fresh file.

You made useful things

That’s not “just learning.”
That’s a portfolio beginning.

You cleaned messy data, sorted text, served a web page, wrote automation that survives bad input, and ran a transparent machine-learning agent.

Plain-language reference

Words worth keeping.

Argument
A value given to a command or function so it knows what to work with.
CSV
A plain-text table where commas separate values.
Directory
Another word for a folder.
Flag
Extra text after a command, like --send, that changes what it does.
Git
A tool that tracks versions of project files on your computer.
GitHub
A website that hosts Git repositories.
JSON
A text format for structured data using lists, objects, and simple values.
Package
Reusable code that can be installed into a Python environment.
pip
Python’s package installation tool.
Repo root
The top folder of the cloned project, containing index.html and tutorials/.
Repository
A project folder and the history Git tracks for it.
Standard library
The tools that already ship with Python, like csv and json.
Terminal
An app for controlling your computer with typed commands.
venv
An isolated set of Python packages for one project.
Working directory
The folder your terminal is currently operating inside.