How to use this page
Find the job you're trying to do, and copy the pattern. Words in capitals, like FILE or NAME, are yours to replace. The examples come from the website checker you build across the track. Every row links to the lesson that explains it. Use the print button at the top to keep a copy next to you.
Two habits that make everything here easier:
- Try it at the
>>>prompt first. Runpython3, type the one line you're unsure of, and see what it gives back.exit()leaves. - Read a traceback from the bottom. The last line names the error; the
File ... linejust above it says where.
When Python says no
Start here when a script stops. Find the last line of the traceback.
| The last line says | It means | Look at | Lesson |
|---|---|---|---|
SyntaxError | Python couldn't understand the file, so nothing ran at all | The line named, and the one just before it: a missing quote, bracket or colon | 1 |
IndentationError | The file's shape is wrong. A line ending in : needs indented lines after it | The spaces at the start of the lines around it | 3 |
NameError: name 'x' is not defined | A name Python hasn't seen: a typo, or a missing import | The spelling, and the imports at the top | 2, 5 |
TypeError | The wrong type for the job, like comparing text with a number, or a function given too few values | What type each value really is: type(x) | 2, 4 |
ValueError: invalid literal for int() | The right type, but a value that makes no sense, like int("n/a") | The data. Catch it with try and except ValueError | 4 |
KeyError: 'status' | The dictionary has no key with that name. Keys must match exactly | The key's spelling, or the file's header line. Use .get() if it might be missing | 3, 5 |
IndexError: list index out of range | A position the list doesn't have. Counting starts at 0 | len(the_list) | 3 |
FileNotFoundError | No file at that path, often because the script ran from another folder | Where you ran it from: pwd | 5 |
ModuleNotFoundError: No module named 'requests' | The package isn't installed for this Python, usually because the virtual environment isn't active | source .venv/bin/activate, then pip install | 6, 8 |
error: unrecognized arguments | A misspelled command-line option | python3 checker.py --help | 8 |
Address already in use | Another copy of your API is already running on that port | The other terminal. Stop it with Ctrl+C | 10 |
Running Python
| To | Run | Lesson |
|---|---|---|
| Check it's installed | python3 --version | 1 |
Try things at the prompt (exit() leaves) | python3 | 1 |
| Run a script | python3 checker.py | 1 |
| Run one line | python3 -c "print(2 + 2)" | 9 |
| Make a virtual environment (once per project) | python3 -m venv .venv | 6 |
| Switch it on (Mac and Linux) | source .venv/bin/activate | 6 |
| Switch it on (Windows) | .venv\Scripts\activate | 6 |
| Install a package into it | pip install requests | 6 |
| Run the tests | pytest | 7 |
| See a script's exit code | echo $? | 7 |
| Start a FastAPI app while you work on it | fastapi dev api.py | 10 |
Values, types and text
| To | Write | Lesson |
|---|---|---|
| Text, a whole number, a decimal, true or false | "example.com", 200, 0.25, True | 2 |
| Ask what type something is | type(200) | 2 |
| Give a value a name | status_code = 200 | 2 |
| Divide, whole-number divide, remainder, power | 7 / 2, 7 // 2, 7 % 2, 2 ** 10 | 2 |
| Compare | ==, !=, <, <=, >, >= | 2 |
| Combine conditions | and, or, not | 2 |
| Round | round(0.256 * 1000) | 2 |
| Join or repeat text | "web" + "site", "-" * 10 | 2 |
| Capitals | "hello".upper() | 1 |
| Text to a number, and back | int("200"), float("0.25"), str(200) | 2 |
| Nothing at all | None, checked with x is None | 6 |
f-strings put values inside text. Anything in braces is worked out, and :.2f means two decimal places:
site = "example.com"
status_code = 200
response_seconds = 0.256
print(f"{site}: {status_code} in {response_seconds:.2f} seconds")
That prints example.com: 200 in 0.26 seconds. Text from the keyboard or a file is always text: "25" * 2 is '2525', so convert first with int().
Lists and dictionaries
A list keeps values in order. A dictionary keeps values with labels, called keys.
| To | Write | Lesson |
|---|---|---|
| Make a list | sites = ["example.com", "example.org"] | 3 |
| First item, last item | sites[0], sites[-1] | 3 |
| How many | len(sites) | 3 |
| Add one to the end | sites.append("example.net") | 3 |
| Is it in the list? | "example.org" in sites | 3 |
| Make a dictionary | site = {"name": "example.com", "status_code": 200} | 3 |
| Read a value | site["name"] | 3 |
| Read a value that might be missing | site.get("owner", "unknown") | 3 |
| Change or add a value | site["status_code"] = 503 | 3 |
| A list of dictionaries: a table of records | sites = [{"name": "a.com"}, {"name": "b.com"}] | 3 |
Decisions and loops
if status_code < 400:
print("up")
elif status_code < 500:
print("client error")
else:
print("down")
for site in sites:
print(site["name"])
attempts = 0
while attempts < 3:
attempts = attempts + 1
The colon and the indented lines after it are the rule: everything indented under if, for or while belongs to it. Use four spaces. for goes through every item; while repeats as long as its condition is true, so make sure something changes it. Lesson 3 covers both.
Functions
def to_ms(seconds):
return round(seconds * 1000)
def fetch(url, max_attempts=3, timeout=5):
...
response_ms = to_ms(0.25)
response = fetch("https://github.com", timeout=10)
| To | Write | Lesson |
|---|---|---|
| Make a function | def name(value):, then indented lines | 4 |
| Hand an answer back | return value. Without it, the function gives back None | 4 |
| A setting with a default | def fetch(url, timeout=5): | 8 |
| Pass a value by name | fetch(url, timeout=10) | 8 |
Errors you expect: try and except
try:
status_code = int(site["status_code"])
except ValueError:
print("status code isn't a number")
status_code = 0
If the try lines raise that error, Python runs the except lines instead of stopping. Name the exact error you expect, like ValueError or requests.RequestException. A bare except: hides the mistakes you'd want to see. Lessons 4 and 6.
Files: CSV and JSON
import csv
import json
def load_sites(path):
with open(path, newline="") as file:
return list(csv.DictReader(file))
def save_results(path, results):
with open(path, "w") as file:
json.dump(results, file, indent=2)
importloads a module from Python's toolbox, at the top of the file.with open(...) as file:opens a file and closes it again when the indented lines finish, even if something fails.csv.DictReaderturns each row into a dictionary, using the header line as the keys. Every value comes back as text.json.dumpsaves lists and dictionaries as JSON;json.load(file)reads them back."w"means write, replacing what was there.
Lesson 5 covers both.
Talking to APIs
import requests
response = requests.get("https://github.com", timeout=5)
response.status_code
response.elapsed.total_seconds()
data = response.json()
requests.post("https://ntfy.sh/TOPIC", data="2 sites down", timeout=10)
Always set timeout, or a server that never answers makes your script wait forever. A failed connection raises requests.RequestException; catch it, wait with time.sleep(1), and try again a few times. A 404 is not an exception: it's an answer, so check status_code. Lessons 6 and 9.
Scripts you can trust
import sys
def main():
...
if down > 0:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
main()holds what the script does, so other files and tests canimportits functions without running it.- The exit code is how cron, CI pipelines and other scripts know it failed: 0 for success, anything else for a problem.
- Tests live in
test_checker.py. Each one is a function whose name starts withtest_, usingassert:
from checker import is_up
def test_400_is_down():
assert not is_up(400)
Lesson 7 covers all three.
Arguments, logging and schedules
| To | Write | Lesson |
|---|---|---|
Take options like --sites work.csv | argparse.ArgumentParser(), add_argument("--sites", default="sites.csv"), then parse_args() | 8 |
| Read an option | args.sites | 8 |
| Log to a file and the screen | logging.basicConfig(level=logging.INFO, handlers=[...]) | 8 |
| Log at a level | logging.info(...), logging.warning(...), logging.error(...) | 8 |
| Edit your schedule | EDITOR=nano crontab -e | 8 |
| Every 15 minutes, with full paths | */15 * * * * /full/path/.venv/bin/python /full/path/checker.py | 8 |
| List your schedule | crontab -l | 8 |
Cron runs with almost nothing set up: no virtual environment and a different folder. That's why the schedule uses full paths, and the .venv's own python.
Settings and secrets
| To | Write | Lesson |
|---|---|---|
| Set a setting for one run | CHECKER_TIMEOUT=10 python3 checker.py | 9 |
| Set it for this terminal | export CHECKER_TIMEOUT=10 | 9 |
| Read it, with a fallback | os.environ.get("CHECKER_TIMEOUT", "5") | 9 |
| Only if it's set | if topic:, since a missing setting is None | 9 |
| A random, hard-to-guess name | python3 -c "import secrets; print(secrets.token_hex(8))" | 9 |
Settings come from the environment, never from the code, so the same script runs anywhere. Secrets especially: a password in the code ends up in Git.
FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="Site checker API")
class SiteResult(BaseModel):
name: str
result: str
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/results/{name}")
def one_result(name: str) -> SiteResult:
for site in load_results():
if site["name"] == name:
return site
raise HTTPException(status_code=404, detail="Site not found")
@app.get("/path")turns the function under it into an endpoint.{name}in the path becomes the function'sname.class SiteResult(BaseModel)describes the shape of the data, and FastAPI checks every answer against it.raise HTTPException(...)answers with an error status, like 404 or 503, instead of crashing./docson your running API lists every endpoint, and lets you try them.- Test it with
TestClient(app), thenclient.get("/health"), in a normalpytesttest.
Lesson 10 builds it all.