Python
The language you’ll write and the program that runs your instructions.
A field guide for first-time coders
Five small, real-world projects. Every command explained. No experience, paid API, or mystery step required.
$ 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.
Work top to bottom. Each project introduces only a few new ideas and reuses what you already learned.
Before the first line of code
About 20 minutes, and you only do it once. Read the six words below first, then work through the six setup steps that follow.
Read these once so the instructions make sense. There is nothing to install or type in this part.
The language you’ll write and the program that runs your instructions.
A text-based way to give your computer precise commands.
A tool that records changes to files, like save points for a project.
A website that stores Git repositories so people can copy and collaborate on them.
A project folder whose files and history Git tracks; “repo” is short for repository.
A venv isolates a project’s packages; pip installs those packages into it.
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.
Setup step 1 of 6 · Account · 3 minutes
One timeOpen github.com/signup in your browser. Enter your email, create a password and username, then complete email verification. A free account is enough.
You can sign in at github.com and see your dashboard.
Setup step 2 of 6 · Install · 5–10 minutes
Choose your OSPython runs your code. Git downloads the project and tracks your changes. Follow only the panel for your computer.
.pkg.git --version. If macOS asks to install developer tools, choose Install.In Terminal or PowerShell, check both installs:
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.
python3 --version
git --version
Windows note The Windows side of each command toggle uses python; the macOS side uses python3. You do not need to translate it yourself.
Python 3.12.x git version 2.x.x
Your exact numbers may be newer.
Setup step 3 of 6 · Download · 2 minutes
TerminalTo clone means to download a Git repository, including its project files and history. First move to your Desktop, then clone.
This is the real course repository. Copy all three lines below exactly. Git downloads every lesson, generator, test, and supporting file.
cd Desktop
git clone https://github.com/cassidythilton/python-from-zero.git
cd python-from-zero
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/.
Setup step 4 of 6 · Editor · 2 minutes
Your choiceAn 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.
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:
cursor .
# or
code .
The editor’s file panel shows index.html and a tutorials folder holding 01-expense-summary through 05-inbox-agent.
Setup step 5 of 6 · Isolate · 3 minutes
TerminalA 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:
python3 -m venv .venv
Now activate it each time you return to this course. Use the macOS/Windows toggle in the command box:
source .venv/bin/activate
(.venv) ...python-from-zero
The exact final symbol varies by computer. The (.venv) at the start means it is active.
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.
PowerShell blocked the script? Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, answer Y, then activate again.
Setup step 6 of 6 · Final check · 1 minute
Readypip 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.
python3 -m pip --version
python3 -c "print('Workbench ready!')"
pip ... from .../.venv/... Workbench ready!
If the pip path contains .venv, your private environment is working.
Your learning path
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.
Turn messy purchases into a clean monthly map.
Turn support messages into useful categories.
Put Python behind a tiny browser-based tool.
Collect JSON records and publish a daily brief.
Train a model, choose tools, and log every step.
Personal finance · 45–60 min
Clean a messy expense file, group purchases by month and category, then save a summary someone could actually use.
What you’re making — and why
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.
amount = 12.50.str), number (float), or something else.parse_date.if statementA choice: run code only when a condition is true.Field procedure
Open your editor’s terminal and confirm (.venv) appears. Every command in this module assumes your terminal is inside tutorials/01-expense-summary.
cd tutorials/01-expense-summaryPrefer 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.
The generator writes the same 37 rows every run, so your screen matches this page. Nothing is downloaded.
python3 generate_data.pyWrote 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.
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.
python3 expense_summary.pyMonthly 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.
A test is a small program that checks your program still behaves.
python3 -m unittest discover -s tests -vRan 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.
Small challenge
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.
Trail check
data/expenses.csv exists with 37 rows.Grand total: $5603.10 and Skipped 2 messy row(s).output/monthly_summary.csv opens as a table of month,category,total,transaction_count."$12.50" has to become the number 12.5.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.
Customer support · 50–70 min
Give incoming support tickets useful tags by scoring keywords, then count what the team should handle.
What you’re making — and why
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.
ticket["subject"].main.py imports tagger.py.Field procedure
Every command here assumes your terminal is inside tutorials/02-ticket-tagger. Pick the line that matches where you are now.
cd ../02-ticket-taggercd tutorials/02-ticket-taggerThis module needs no packages—json ships with Python. Now create the ticket file:
python3 generate_data.pyWrote 12 tickets to .../tutorials/02-ticket-tagger/data/tickets.json
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.
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.
python3 main.pyTagged 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.
In tagger.py, add "display name" to the login keyword list, save, and run python3 main.py again.
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.
python3 -m unittest discover -s tests -vRan 15 tests in 0.0..s OK
Small challenge
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.
Trail check
data/tickets.json holds 12 tickets.total: 12.output/tagged_tickets.json exists and each dictionary has a "tag" key.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.
Internal tools · 60–90 min
Build a tiny web app that lists stock, searches it, and adjusts quantities—running only on your own computer.
What you’re making — and why
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.
/search, connected to a Python function.? in a web address, such as ?q=coffee.Field procedure
Every command here assumes your terminal is inside tutorials/03-flask-inventory.
cd ../03-flask-inventorycd tutorials/03-flask-inventoryThis is the first module that needs a package. Check that your prompt shows (.venv), then install what requirements.txt lists.
python3 -m pip install -r requirements.txtSuccessfully installed Flask-3.x.x ...
Version numbers and extra package names may differ.
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.
python3 generate_data.pyWrote 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.
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.
python3 app.py* 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.
The home page shows a line like 24 items in stock list · 5 at or below 10 units above a 24-row table.
coffee. The address becomes /search?q=coffee and 2 matching items remain.zzzzz. You get the Nothing matched “zzzzz” message instead of an error.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.
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:
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.
Click the terminal and press Control + C (Control, not Command, on a Mac). Your prompt returns and the page stops loading—that is expected.
python3 -m unittestRan 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.
Small challenge
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.
Trail check
127.0.0.1:5000.coffee returns 2 rows.zzzzz shows the “Nothing matched” message.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.
Automation · 55–75 min
Turn a folder of daily JSON exports into one tidy summary that does not fall over when a file is junk.
What you’re making — and why
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.
try / excepttry means “attempt this”; except means “if it fails, do this instead of crashing.”"amount".reports/summary.txt..py file another can import; the tests here import report_builder.Field procedure
Every command here assumes your terminal is inside tutorials/04-json-report. This module needs no packages.
cd ../04-json-reportcd tutorials/04-json-reportpython3 generate_data.pyWrote 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.
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.
python3 report_builder.pyReading 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.
Open reports/summary.txt. It ends like this:
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).
python3 report_builder.py --sendWrote 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.
python3 -m unittestRan 16 tests in 0.0..s OK
The tests use temporary folders, so they never touch your data/, reports/, or outbox/.
Small challenge
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.
Trail check
data/daily/ holds 5 JSON files.reports/summary.txt exists and says PROBLEMS FOUND: 4.$275.45.outbox/report_ready.txt appears only after you pass --send.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.
Helpdesk automation · 75–90 min
Train a small model on generated emails, then run a loop that picks tools from a fixed list and logs every decision.
What you’re making — and why
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.
Field procedure
Every command here assumes your terminal is inside tutorials/05-inbox-agent.
cd ../05-inbox-agentcd tutorials/05-inbox-agentpython3 -m pip install -r requirements.txtSuccessfully 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.
python3 generate_data.pyWrote 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.
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.
python3 train_model.pySaved 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.
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.
python3 agent.pyInbox 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.
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.
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.
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.
python3 -m unittest discover -s tests -vRan 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.
Small challenge
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?”
Trail check
model/inbox_model.joblib exists after training.Stop: line and logs/agent_log.jsonl gains 8 lines.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
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
--send, that changes what it does.index.html and tutorials/.csv and json.