The patterns that make real backend code clean & maintainable — modules, errors, APIs, config, classes. AI writes it; you read it, judge it, steer it.
← / → to navigate · EN/VI switch below
Basics let a script run. Advanced lets you live inside a real project — the skill this deck builds.
AI generates most of this code fluently. Your job is to recognize what each block is doing and whether it's right.
This deck is a map of WHY things exist and where they fit. Exact spelling — you look up or ask the AI.
Every piece lands in a real Flask project: config, error handling, API calls, files, logs, models. Nothing is abstract.
Modules & environments → errors → compact data & APIs → plumbing (config, files, logs) → OOP → big picture.
How code is organized — and how a project gets its packages — before a single line runs.
Big programs don't fit in one file. Modules are how Python splits work into named boxes.
A module is a named box of functions, classes and data — json, os, requests, and your own files too.
import json loads the module into your file; json.loads(...) then reaches into it. One line = a toolbox on the table.
Grab only the pieces you need: from utils import clean_name — shorter, cleaner call sites.
main.py + routes.py + models.py + utils.py. Imports are how these files talk to each other.
Three shapes: whole module, specific pieces, and your own project's modules.
import json # standard library — ships with Python
import requests # installed with pip (see requirements.txt)
from utils import clean_name # a module in YOUR project
from models import User
print(json.__file__) # proof: every module is just a .py file
Standard library, pip packages, and your own files — all enter through the same door.
Your laptop has a Python. Your project gets its own copy — isolated, versioned, disposable.
python -m venv .venv creates a folder with its own python + its own packages. Nothing leaks in or out.
Project A needs requests 2.32, project B needs 2.28 — both install fine, side by side, each in its own room.
venv + requirements.txt = the identical setup on your laptop, the server, and any AI sandbox.
source .venv/bin/activate — from then on, pip installs land in THIS project only. Deactivate when done.
The four commands every Python project starts with.
pip installs packages; requirements.txt is the shopping list that makes it repeatable.
pip install requests fetches a package from PyPI (Python's app store) into your venv.
Every package with its exact version. pip freeze > requirements.txt writes the list of what's installed now.
New machine: create venv → activate → pip install -r requirements.txt. Same project, same behavior.
Tell the AI what you need; it adds the package and updates requirements.txt. Check that it actually did.
Real software meets bad input, missing files and slow networks. Survive them on purpose.
Every one of these will happen in production. The question is what your app does when it does.
A user types letters into a number field. The file is missing. The API times out. The disk fills up.
An uncaught exception kills the whole process — every request after it fails too. One bad input takes down the site.
Catch the predictable failure and fall back gracefully: a default value, a retry, a kind error message.
Log the surprise, return a clean 500. The user sees calm; you get the details to fix it.
One shape, three jobs: attempt the risky part, recover from known failure, always tidy up.
def load(path):
try:
with open(path) as f: # the risky part
return f.read()
except FileNotFoundError: # one known failure
print("no such file:", path)
return "" # graceful fallback
finally:
print("done") # ALWAYS runs
try = the attempt · except = the plan B for a named error · finally = cleanup that can't be skipped.
Sometimes the right move is to refuse: raise an exception that names the problem.
def set_age(user, value):
if not isinstance(value, int):
raise TypeError("age must be an int") # stop, with a reason
try:
set_age(user, "thirty") # boom: TypeError
except TypeError as e: # e carries the message
logging.warning("bad input: %s", e)
raise = a structured 'no, and here's why'. Catching it far away keeps the crash message useful.
Two habits that separate careful code from lucky code — and red flags to spot in AI output.
except FileNotFoundError: handles exactly that case. A bare except: swallows everything — including real bugs — and hides them.
Spot bare excepts in AI code: they're usually a red flag, not a virtue.
In development, let the unexpected crash — the traceback tells you exactly where. In production, handle the expected, log the unexpected.
Rule of thumb: handle the input, log the surprise.
Comprehensions, JSON, and calling other people's APIs with requests.
The most common 'advanced' idiom. It builds a list from a collection — filter + transform.
prices = [1200, 45, 999, 12]
# the plain loop
over_100 = []
for p in prices:
if p > 100:
over_100.append(p)
# the same thing, one lineover_100 = [p for p in prices if p > 100] # [1200, 999]
Read right-to-left: take each p in prices, keep it if p > 100, collect the results.
Same idea with braces: build a dict from a loop — the go-to move for lookups by id.
users = [{"id": 1, "name": "An"},
{"id": 2, "name": "Minh"},
{"id": 3, "name": "Linh"}]
# build a dict from a loop: id -> name
by_id = {u["id"]: u["name"] for u in users}
# {1: "An", 2: "Minh", 3: "Linh"}
One line turns a list of records into an instant lookup table — you'll see this everywhere.
Browsers, APIs, databases, and your Flask app all speak it. In Python it's almost free.
{"name": "An", "score": 9} and [1, 2, 3] — your dict/list, written as text that travels over the network.
JSON text → Python dict/list. This is what you do with an API response body.
dict/list → JSON text, ready to send back to a browser or another service.
Python's dict and list ARE JSON's object and array. No conversion dance — just the text boundary.
Two functions, two directions — the only JSON you'll ever need.
import json
text = '{"name": "An", "score": 9}' # JSON in (e.g. from an API)
data = json.loads(text) # text -> dict
print(data["score"]) # 9
out = json.dumps(data, indent=2) # dict -> text
print(out) # back to JSON, for the wire
loads = load (text in), dumps = dump (text out). The names describe the direction.
Your app rarely does everything alone. The requests library turns HTTP into two lines.
One call = one HTTP request. get = fetch, post = submit or create. The URL is the argument.
Parse the response body as JSON straight into a dict/list — no manual json.loads needed.
200 OK, 404 missing, 500 their bug. Always check it before trusting the body.
Auth tokens and content types ride along: headers={"Authorization": ...} on every protected call.
The exact shape you'll find in half of any backend's code.
import requests
r = requests.get("https://api.example.com/users",
headers={"Authorization": "Bearer " + TOKEN})
if r.status_code == 200: # 2xx = success
users = r.json() # response text -> list of dicts
else:
logging.error("API error: %s", r.status_code)
Call → check the verdict → parse the payload. That's the whole dance.
Config out of the code, files that clean up after themselves, and logs instead of prints.
The same code must run on your laptop, staging and production — with different values.
DB URLs, API keys, tokens: read from os.environ at runtime, never hard-coded in a .py file.
One codebase, three environments with different values — and secrets never sit in the repo (or in AI chat history).
A text file of KEY=VALUE pairs loaded into the environment; it goes in .gitignore and is never committed.
.get("KEY") → None if missing; .get("KEY", default) for optional settings. Raise loudly for required ones.
Optional with a default, or required with a loud failure — never a silent secret.
import os
DB_URL = os.environ.get("DB_URL", "sqlite:///dev.db") # optional
SECRET = os.environ.get("SECRET") # required
if SECRET is None: # fail loudly, at startup
raise RuntimeError("SECRET not set")
The app refuses to boot without what it needs — a crash on start beats a mystery at 3 a.m.
Resources (files, database connections) are borrowed. with promises they'll be returned.
Whatever it opens is closed when the block ends — even if an error happens halfway through.
with open(path) as f: — no forgotten f.close(), no half-written files after a crash.
The same pattern everywhere: connections must be released or they leak, one by one, until the app dies.
with X as y: ... anywhere = 'y is borrowed, and returned when done'. You'll never read it wrong again.
Read and write, two lines of ceremony, zero cleanup on your mind.
# 'with' = the file closes for you, even on error
with open("data.csv") as f: # read (default mode)
for line in f: # a file is a list of lines
print(line.strip())
with open("out.txt", "w") as f: # 'w' = write (erase first)
f.write("hello\n")
# <- already closed; no f.close() needed
w erases first, a appends, r reads. with makes the closing automatic.
The single most common 'works on my machine' habit — and the fix for it.
Fine during development: quick checks on your own machine. It goes to the console and disappears.
No timestamps, no levels, no control over where it goes. In production, print output is often lost entirely.
Levels (DEBUG / INFO / WARNING / ERROR), timestamps, and output to files or a log service you can query later.
Logs are the diary that survives: what happened, when, at what severity — the first place you look when something breaks.
Set the level once; then the app narrates its life.
import logging
logging.basicConfig(level=logging.INFO)
logging.info("server starting on port 9151")
logging.warning("low disk space")
logging.error("failed to reach API")
# 2026-10-27 00:36:59,123 INFO __main__: server starting...
Every line carries a timestamp and a severity — searchable, sortable, filterable. print never could.
A class bundles data + behavior. Enough to read a model — not to design a framework.
The one 'big' concept — and really just a named shape for a kind of thing.
A class defines a kind of thing: its fields (data) and its methods (what it can do). One idea, one home.
User, Order, Invoice — the shape of the real world, one class each. That's 80% of what OOP is for in backends.
Runs when you create one: User("An", "an@x.com") fills in the fields. You'll see it in every model.
Inside a method, self points at the particular instance: me.name reads ME's name, not someone else's.
Ten lines: data in __init__, one behavior, one instance. That's the whole pattern.
class User:
def __init__(self, name, email): # runs on User(...)
self.name = name # data on THIS user
self.email = email
def is_admin(self): # a method: behavior
return self.email.endswith("@corp.com")
me = User("An", "an@corp.com")
print(me.name, me.is_admin()) # An True
class = blueprint · User(...) = one concrete thing · me.name / me.is_admin() = its data and behavior.
You won't design frameworks — but you will open models.py daily. Here's the skim.
self.name, self.email: what the thing knows. The attributes ARE the schema — they tell you what data exists and under what name.
Skim __init__ first: it lists every field in reading order.
user.is_admin(): what the thing does — usually attributes plus a bit of logic.
Attributes + methods = the whole contract of the model. You can now read any model the AI writes.
Every piece, where it lives in a real Flask app — and how you read & extend with AI.
Open the course's own app and every pattern from this deck is in it.
os.environ at the top of the app: secrets & settings live in the environment, never in the code.
Every risky spot wrapped in try/except: file reads, API calls, user input. The app degrades — it doesn't die.
Comprehensions + json: read the wire, reshape into dicts & lists, send it back. requests talks to the outside.
Classes for User / Order; with keeps files & connections honest; logging keeps the diary. That's the whole stack.
A cheat-sheet for reading any backend project.
| Piece | Where you'll see it |
|---|---|
| import / modules | Every .py file; main.py pulls in routes, models, utils |
| venv + requirements.txt | Project root: .venv/ folder; new machine = activate + install -r |
| try / except / raise | Around file reads, API calls, user input — the app must not die |
| comprehensions | Shaping data before it leaves: lists for tables, dicts for lookups |
| json + requests | Talking to external APIs: r.json() in, json.dumps out |
| os.environ | Config & secrets: DB URL, API keys — never in the code |
| with / context managers | Reading & writing files, opening database connections |
| logging | The app's diary: what happened, when, at what severity |
| class | Models: User, Order, Slide — data + behavior per entity |
The daily loop, now with a bigger vocabulary.
Scan the AI's code before asking anything: import? try/except? with open? a class? You now know what each shape is for.
Paste the block: 'explain this in simple words.' Compare the AI's answer with your own read — the gap is where learning happens.
Target a specific line or behavior. Small diffs stay verifiable; big asks hide mistakes.
Output and the logs are the truth. Something broke? Paste the traceback — you now know where the real error lives.
The AI can write the code. The one who can read it, judge it and extend it is the shipper.
Next: Flask & HTTP — this same toolkit, now answering web requests. Until then: open any code and ask the AI to explain it line by line.