→ From Coder to Shipper
From Coder to Shipper · Backend foundations

Python — beyond the basics

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

Big picture

From 'does it work?' to 'can you read it?'

Basics let a script run. Advanced lets you live inside a real project — the skill this deck builds.

You read, the AI writes

AI generates most of this code fluently. Your job is to recognize what each block is doing and whether it's right.

Patterns, not syntax

This deck is a map of WHY things exist and where they fit. Exact spelling — you look up or ask the AI.

It's all one app

Every piece lands in a real Flask project: config, error handling, API calls, files, logs, models. Nothing is abstract.

The map

Modules & environments → errors → compact data & APIs → plumbing (config, files, logs) → OOP → big picture.

01
Part 1

Modules & environments

How code is organized — and how a project gets its packages — before a single line runs.

Part 1 · modules

A file is a module

Big programs don't fit in one file. Modules are how Python splits work into named boxes.

Every .py file is a module

A module is a named box of functions, classes and data — json, os, requests, and your own files too.

import pulls it in

import json loads the module into your file; json.loads(...) then reaches into it. One line = a toolbox on the table.

from ... import ...

Grab only the pieces you need: from utils import clean_name — shorter, cleaner call sites.

Your project is modules

main.py + routes.py + models.py + utils.py. Imports are how these files talk to each other.

Part 1 · modules

Imports in a real file

Three shapes: whole module, specific pieces, and your own project's modules.

app/main.py
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.

Part 1 · environments

venv — one project, one Python

Your laptop has a Python. Your project gets its own copy — isolated, versioned, disposable.

A private room for the project

python -m venv .venv creates a folder with its own python + its own packages. Nothing leaks in or out.

No more conflicts

Project A needs requests 2.32, project B needs 2.28 — both install fine, side by side, each in its own room.

Reproducible by design

venv + requirements.txt = the identical setup on your laptop, the server, and any AI sandbox.

You activate it

source .venv/bin/activate — from then on, pip installs land in THIS project only. Deactivate when done.

Part 1 · environments

venv in practice

The four commands every Python project starts with.

ops@vps — zsh
$ python -m venv .venv
$ source .venv/bin/activate
(venv) $ pip install requests
Collecting requests
Successfully installed requests-2.32.3
(venv) $ pip freeze > requirements.txt
(venv) $ pip install -r requirements.txt # rebuild on another machine
(venv) $ deactivate
Part 1 · pip

pip & requirements.txt

pip installs packages; requirements.txt is the shopping list that makes it repeatable.

pip = the package installer

pip install requests fetches a package from PyPI (Python's app store) into your venv.

requirements.txt = shopping list

Every package with its exact version. pip freeze > requirements.txt writes the list of what's installed now.

Rebuild anywhere

New machine: create venv → activate → pip install -r requirements.txt. Same project, same behavior.

The AI uses it too

Tell the AI what you need; it adds the package and updates requirements.txt. Check that it actually did.

02
Part 2

Error handling

Real software meets bad input, missing files and slow networks. Survive them on purpose.

Part 2 · why

The world is unreliable

Every one of these will happen in production. The question is what your app does when it does.

It happens

A user types letters into a number field. The file is missing. The API times out. The disk fills up.

Unhandled = dead

An uncaught exception kills the whole process — every request after it fails too. One bad input takes down the site.

Handle what you expect

Catch the predictable failure and fall back gracefully: a default value, a retry, a kind error message.

Report what you can't

Log the surprise, return a clean 500. The user sees calm; you get the details to fix it.

Part 2 · the shape

try / except / finally

One shape, three jobs: attempt the risky part, recover from known failure, always tidy up.

app/files.py
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.

Part 2 · raising

raise — stop with a reason

Sometimes the right move is to refuse: raise an exception that names the problem.

app/guard.py
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.

Part 2 · judgment

Specific catches; deliberate crashes

Two habits that separate careful code from lucky code — and red flags to spot in AI output.

Catch specific, not everything

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.

When to handle vs let it crash

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.

03
Part 3

Compact data & the outside world

Comprehensions, JSON, and calling other people's APIs with requests.

Part 3 · comprehensions

List comprehension — a loop in one line

The most common 'advanced' idiom. It builds a list from a collection — filter + transform.

app/comps.py
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.

Part 3 · comprehensions

Dict comprehension — building lookups

Same idea with braces: build a dict from a loop — the go-to move for lookups by id.

app/comps2.py
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.

Part 3 · JSON

JSON — the wire format of the web

Browsers, APIs, databases, and your Flask app all speak it. In Python it's almost free.

JSON = text for dict & list

{"name": "An", "score": 9} and [1, 2, 3] — your dict/list, written as text that travels over the network.

json.loads — in

JSON text → Python dict/list. This is what you do with an API response body.

json.dumps — out

dict/list → JSON text, ready to send back to a browser or another service.

Your types already speak it

Python's dict and list ARE JSON's object and array. No conversion dance — just the text boundary.

Part 3 · JSON

json.loads / json.dumps

Two functions, two directions — the only JSON you'll ever need.

app/jsondemo.py
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.

Part 3 · APIs

Calling other people's APIs

Your app rarely does everything alone. The requests library turns HTTP into two lines.

requests.get / requests.post

One call = one HTTP request. get = fetch, post = submit or create. The URL is the argument.

r.json() — the payload

Parse the response body as JSON straight into a dict/list — no manual json.loads needed.

status_code — the verdict

200 OK, 404 missing, 500 their bug. Always check it before trusting the body.

headers — the envelope

Auth tokens and content types ride along: headers={"Authorization": ...} on every protected call.

Part 3 · APIs

An API call, end to end

The exact shape you'll find in half of any backend's code.

app/api.py
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.

04
Part 4

Real-app plumbing

Config out of the code, files that clean up after themselves, and logs instead of prints.

Part 4 · config

Environment variables — secrets out of the code

The same code must run on your laptop, staging and production — with different values.

Config lives outside the code

DB URLs, API keys, tokens: read from os.environ at runtime, never hard-coded in a .py file.

Why it matters

One codebase, three environments with different values — and secrets never sit in the repo (or in AI chat history).

.env files

A text file of KEY=VALUE pairs loaded into the environment; it goes in .gitignore and is never committed.

os.environ.get

.get("KEY") → None if missing; .get("KEY", default) for optional settings. Raise loudly for required ones.

Part 4 · config

Reading config from the environment

Optional with a default, or required with a loud failure — never a silent secret.

app/config.py
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.

Part 4 · context managers

with — guaranteed cleanup

Resources (files, database connections) are borrowed. with promises they'll be returned.

with = always released

Whatever it opens is closed when the block ends — even if an error happens halfway through.

Files

with open(path) as f: — no forgotten f.close(), no half-written files after a crash.

Databases & sockets

The same pattern everywhere: connections must be released or they leak, one by one, until the app dies.

Recognize the shape

with X as y: ... anywhere = 'y is borrowed, and returned when done'. You'll never read it wrong again.

Part 4 · files

File I/O with with

Read and write, two lines of ceremony, zero cleanup on your mind.

app/files.py
# '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.

Part 4 · logging

print is for you; logging is for the app

The single most common 'works on my machine' habit — and the fix for it.

print — for you, right now

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.

logging — for the app, always

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.

Part 4 · logging

logging in five lines

Set the level once; then the app narrates its life.

app/logdemo.py
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.

05
Part 5

OOP — lightly

A class bundles data + behavior. Enough to read a model — not to design a framework.

Part 5 · classes

A class bundles data + behavior

The one 'big' concept — and really just a named shape for a kind of thing.

Data + behavior, one box

A class defines a kind of thing: its fields (data) and its methods (what it can do). One idea, one home.

Model real entities

User, Order, Invoice — the shape of the real world, one class each. That's 80% of what OOP is for in backends.

__init__ — the constructor

Runs when you create one: User("An", "an@x.com") fills in the fields. You'll see it in every model.

self — this particular one

Inside a method, self points at the particular instance: me.name reads ME's name, not someone else's.

Part 5 · classes

The smallest useful class

Ten lines: data in __init__, one behavior, one instance. That's the whole pattern.

app/models.py
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.

Part 5 · reading models

How to read a model in 30 seconds

You won't design frameworks — but you will open models.py daily. Here's the skim.

Attributes = the data

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.

Methods = the behavior

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.

06
Part 6

The big picture

Every piece, where it lives in a real Flask app — and how you read & extend with AI.

Part 6 · the toolkit

This is how a real Flask app stays alive

Open the course's own app and every pattern from this deck is in it.

Config from the outside

os.environ at the top of the app: secrets & settings live in the environment, never in the code.

Errors contained

Every risky spot wrapped in try/except: file reads, API calls, user input. The app degrades — it doesn't die.

Data shaped compactly

Comprehensions + json: read the wire, reshape into dicts & lists, send it back. requests talks to the outside.

Models mirror reality

Classes for User / Order; with keeps files & connections honest; logging keeps the diary. That's the whole stack.

Part 6 · map

Where each piece shows up

A cheat-sheet for reading any backend project.

PieceWhere you'll see it
import / modulesEvery .py file; main.py pulls in routes, models, utils
venv + requirements.txtProject root: .venv/ folder; new machine = activate + install -r
try / except / raiseAround file reads, API calls, user input — the app must not die
comprehensionsShaping data before it leaves: lists for tables, dicts for lookups
json + requestsTalking to external APIs: r.json() in, json.dumps out
os.environConfig & secrets: DB URL, API keys — never in the code
with / context managersReading & writing files, opening database connections
loggingThe app's diary: what happened, when, at what severity
classModels: User, Order, Slide — data + behavior per entity
Part 6 · workflow

Reading & extending code with AI

The daily loop, now with a bigger vocabulary.

  1. 1

    Spot the pattern

    Scan the AI's code before asking anything: import? try/except? with open? a class? You now know what each shape is for.

  2. 2

    Ask what it's doing

    Paste the block: 'explain this in simple words.' Compare the AI's answer with your own read — the gap is where learning happens.

  3. 3

    Change one thing at a time

    Target a specific line or behavior. Small diffs stay verifiable; big asks hide mistakes.

  4. 4

    Run it, read the output

    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.
— the course

Modules. Errors. JSON. Config. Classes. That's real backend Python.

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.