Free Handbook · Runs in your browser

Python + Tools

Nine mini-labs on how Python is actually used at work: SQL from sqlite3 to Postgres, pandas, FastAPI, HTTP APIs with requests, Docker, AWS with boto3, Spark through PySpark, Airflow, and Git — each with the real code you would write on day one.

0 / 108 lessons🔥 0 day streak
ShareXLinkedIn

Module 14 · what you'll be able to do

  • Query a database from Python safely, with parameters, and know the sqlite3 → psycopg path
  • Load, filter, group and join data in pandas and know when to reach for it
  • Stand up a typed JSON API with FastAPI in twenty lines
  • Call an HTTP API with requests, handle errors and paginate
  • Containerise a Python app and talk to AWS from code
  • Recognise PySpark and Airflow code and where the /learn courses take you deeper
  • Use Git the way a Python team does, including the .gitignore that matters
01

Python + SQL

Every Python job touches a database. The standard library ships sqlite3, a complete SQL engine in one file — and its API (connect → cursor → execute → fetch) is the same DB-API every other driver follows, so what you learn here transfers to Postgres, MySQL and Snowflake. This one runs right here.

pythonEdit it. ⌘/Ctrl + Enter runs.
You should see
cy 210.0
ada 120.0
bob 35.5
3 orders over 50
orders table still exists: 4 rows
Your turn
Add a created column, insert dates as ISO strings, and select the orders from a given month with a parameter.
Python + PostgreSQL

The same code against PostgreSQL

Swap the driver, keep the shape. psycopg (v3) is the standard Postgres driver; parameters are %s instead of ?, and a connection is a context manager that commits on success and rolls back on error.

python
import psycopg                                   # pip install "psycopg[binary]"

with psycopg.connect("postgresql://app:secret@localhost:5432/shop") as con:
    with con.cursor() as cur:
        cur.execute(
            "SELECT customer, SUM(total) FROM orders WHERE status = %s GROUP BY customer",
            ("paid",),
        )
        for customer, spent in cur.fetchall():
            print(customer, spent)
    # leaving the block commits; an exception rolls back
Learn the SQL itself — SQL Mastery, runnable in the browser →
02

Python + pandas

pandas is the spreadsheet-in-code that data teams live in: a DataFrame is a table with named columns, and one line does what a loop would take ten. Reach for it when data is tabular and fits in memory (up to a few GB); stay with plain Python for small lists and dicts, and move to Spark when it does not fit on one machine.

Python + pandas

Load, clean, group, join — the daily loop

Everything you did with dicts in Module 03 has a one-line pandas equivalent. The mental model: operations apply to whole columns at once (vectorised), and most return a new DataFrame rather than changing the old one.

python
import pandas as pd                                    # pip install pandas

orders = pd.read_csv("orders.csv", parse_dates=["created"])
customers = pd.read_json("customers.json")

# look
print(orders.head()); print(orders.dtypes); print(orders.describe())

# clean
orders = orders.dropna(subset=["total"])
orders["total"] = orders["total"].astype(float)
orders = orders[orders["status"] == "paid"]           # filter = boolean mask

# derive
orders["month"] = orders["created"].dt.to_period("M")

# group (the Module 03 counting loop, in one line)
by_customer = orders.groupby("customer")["total"].agg(["sum", "count"]).sort_values("sum", ascending=False)

# join
report = by_customer.merge(customers, left_index=True, right_on="name", how="left")

# out
report.to_csv("report.csv", index=False)
print(report.head(10))
Where pandas fits in a pipeline — the Data Engineering course →
The three pandas mistakes
Looping over rows with iterrows() (use a vectorised expression instead); chained indexing df[a][b] = x (use .loc[a, b]); and forgetting that most methods return a copy — df.dropna() alone does nothing.
03

Python + FastAPI

FastAPI is how modern Python teams ship an HTTP API. You write typed functions; it gives you request validation, JSON serialisation, error responses and interactive documentation at /docs for free. The type hints from Module 09 are not decoration here — they are the contract.

Python + FastAPI

A complete JSON API in twenty lines

Pydantic models define the shape of the input and output; a wrong type in the request becomes a 422 with a clear message before your code runs. uvicorn main:app --reload serves it.

python
from fastapi import FastAPI, HTTPException          # pip install fastapi uvicorn
from pydantic import BaseModel, Field

app = FastAPI(title="Orders API")

class OrderIn(BaseModel):
    customer: str = Field(min_length=1)
    total: float = Field(gt=0)

class Order(OrderIn):
    id: int

DB: dict[int, Order] = {}

@app.post("/orders", response_model=Order, status_code=201)
def create_order(body: OrderIn) -> Order:
    order = Order(id=len(DB) + 1, **body.model_dump())
    DB[order.id] = order
    return order

@app.get("/orders/{order_id}", response_model=Order)
def get_order(order_id: int) -> Order:
    if order_id not in DB:
        raise HTTPException(404, "no such order")
    return DB[order_id]

# $ uvicorn main:app --reload
# $ curl -X POST localhost:8000/orders -H "content-type: application/json" -d '{"customer":"ada","total":12.5}'
# {"customer":"ada","total":12.5,"id":1}
Building integrations around an API — the FDE course →
04

Python + HTTP APIs (requests)

Calling someone else's API is the other half. requests is the library everyone uses; the standard-library urllib works too but is clumsier. The parts that matter in production are the ones tutorials skip: timeouts, status checks, retries with backoff, and pagination. This page has no network access, so the example is for your machine.

Python + Python

A GET that survives contact with the real world

Always pass a timeout — without one a hung server hangs your program forever. Always check the status with raise_for_status(). Read the API's pagination scheme and loop until it says stop.

python
import time
import requests                                          # pip install requests

session = requests.Session()                             # reuses the connection; set auth once
session.headers["Authorization"] = "Bearer " + "YOUR_TOKEN"

def get_json(url, params=None, tries=3):
    for attempt in range(1, tries + 1):
        try:
            r = session.get(url, params=params, timeout=10)
            if r.status_code == 429 or r.status_code >= 500:       # rate-limited or server error: retry
                raise requests.HTTPError(f"{r.status_code} from {url}")
            r.raise_for_status()                                    # 4xx → exception with the reason
            return r.json()
        except (requests.ConnectionError, requests.Timeout, requests.HTTPError) as e:
            if attempt == tries:
                raise
            time.sleep(2 ** attempt)                                # 2, 4, 8 seconds

def all_pages(url):
    page = 1
    while True:
        data = get_json(url, params={"page": page, "per_page": 100})
        yield from data["items"]
        if not data.get("has_more"):
            break
        page += 1

for item in all_pages("https://api.example.com/orders"):
    print(item["id"])
Quick check

A script calling an API "sometimes hangs for hours" in production. Most likely missing?

05

Python + Docker

Docker packages your program with its exact Python version and packages into an image that runs the same on your laptop, CI and the server — the permanent fix for "works on my machine". A Python Dockerfile is short and almost always the same shape.

Python + Docker

The Dockerfile every Python service starts from

Copy requirements.txt first and install, then copy the code: Docker caches each layer, so a code change does not reinstall every package. Run as a non-root user. Pin the base image.

python
# Dockerfile
FROM python:3.13-slim

WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

# dependencies first — this layer is cached until requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# then the code
COPY . .
RUN useradd -m app && chown -R app /app
USER app

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

# $ docker build -t orders-api .
# $ docker run -p 8000:8000 --env-file .env orders-api
Deploying inside a customer's environment — the FDE course →
06

Python + AWS (boto3)

boto3 is the AWS SDK for Python: every service is a client with methods named after the API. The patterns you need on day one are S3 (upload, download, list), and a Lambda handler — a plain function AWS calls with an event dict. Credentials come from the environment or an IAM role, never from code.

Python + AWS

S3 in five calls, and a Lambda handler

boto3 reads credentials from ~/.aws/credentials, environment variables, or the instance role — configure once, never hard-code. Paginators handle the 1,000-key page limit for you.

python
import json
import boto3                                              # pip install boto3

s3 = boto3.client("s3")

s3.upload_file("report.csv", "my-bucket", "reports/2026-09/report.csv")
s3.download_file("my-bucket", "raw/orders.json", "orders.json")

obj = s3.get_object(Bucket="my-bucket", Key="raw/orders.json")
orders = json.loads(obj["Body"].read())                   # read straight into memory

for page in s3.get_paginator("list_objects_v2").paginate(Bucket="my-bucket", Prefix="raw/"):
    for o in page.get("Contents", []):
        print(o["Key"], o["Size"])

# lambda_function.py — AWS calls this with the triggering event
def handler(event, context):
    for record in event["Records"]:                       # e.g. an S3 "object created" event
        bucket = record["s3"]["bucket"]["name"]
        key = record["s3"]["object"]["key"]
        print(f"new file: s3://{bucket}/{key}")
    return {"statusCode": 200}
Where S3 and Lambda sit in a data platform — the Data Engineering course →
07

Python + Spark (PySpark)

When the data does not fit on one machine, Spark spreads it across many — and PySpark lets you drive it from Python with an API that looks a lot like pandas. The difference is laziness: nothing runs until an action (show, count, write), and every transformation is a plan Spark optimises first.

Python + Apache Spark

The same group-and-join, on a cluster

Read, filter, group, join, write — the pandas loop with the same names, running on however many machines the cluster has. explain() shows the plan Spark will run.

python
from pyspark.sql import SparkSession, functions as F   # pip install pyspark

spark = SparkSession.builder.appName("orders").getOrCreate()

orders = spark.read.parquet("s3://my-bucket/orders/")      # billions of rows is fine
customers = spark.read.json("s3://my-bucket/customers/")

report = (
    orders
    .filter(F.col("status") == "paid")
    .withColumn("month", F.date_trunc("month", "created"))
    .groupBy("customer", "month")
    .agg(F.sum("total").alias("spent"), F.count("*").alias("orders"))
    .join(customers, on="customer", how="left")
)

report.explain()                                           # the physical plan — nothing has run yet
report.write.mode("overwrite").partitionBy("month").parquet("s3://my-bucket/reports/")   # the action
Every Spark function, runnable — the Spark course →
08

Python + Airflow

Airflow schedules and monitors pipelines. A DAG is a Python file that declares tasks and the order they run in; Airflow runs them on a schedule, retries failures, and shows you a graph of what happened. Your Python functions become tasks with one decorator.

Python + Airflow

A three-task daily pipeline

Each @task is a normal function; returning a value passes it to the next task. The >> is the dependency graph. Everything else — schedule, retries, alerts, backfills — is configuration.

python
from datetime import datetime, timedelta
from airflow.decorators import dag, task                 # pip install apache-airflow

@dag(schedule="0 6 * * *", start_date=datetime(2026, 1, 1), catchup=False,
     default_args={"retries": 2, "retry_delay": timedelta(minutes=5)})
def daily_orders():

    @task
    def extract() -> str:
        path = "/tmp/orders_raw.json"
        # ... call the API from the requests lesson, write the file
        return path

    @task
    def transform(path: str) -> str:
        # ... pandas: clean, group, write /tmp/report.csv
        return "/tmp/report.csv"

    @task
    def load(report: str) -> None:
        # ... boto3: upload to S3, or psycopg: insert into Postgres
        ...

    load(transform(extract()))      # the dependency graph: extract >> transform >> load

daily_orders()
Orchestration in context — the Data Engineering course →
09

Python + Git

Git is not Python-specific, but a Python repo has its own conventions and its own things to keep out. Every job expects you to branch, commit small, write a message that says why, and open a pull request. Here is the daily loop and the .gitignore every Python project needs on day one.

Python + Git

The daily loop, and what never gets committed

The virtual environment, caches, secrets and generated output never go into the repository. requirements.txt (or pyproject.toml + a lockfile) does — that is how the next person rebuilds the environment.

python
# .gitignore — the Python essentials
.venv/
__pycache__/
*.pyc
.env                 # secrets — NEVER commit
.pytest_cache/
.mypy_cache/
*.egg-info/
dist/ build/
.ipynb_checkpoints/
data/                # large or private inputs; keep a small sample/ instead

# the loop
$ git switch -c feature/order-report          # a branch per change
$ python -m pytest                            # green before you commit
$ git add -p                                  # review each hunk you stage
$ git commit -m "report: group paid orders by month

Refunded orders were counted as revenue; filter on status first."
$ git push -u origin feature/order-report     # then open a pull request
Code reviewers look for these — the Design Patterns handbook →
A committed secret is public forever
Removing a key in the next commit does not remove it from history. Rotate the key immediately, then clean history. Put secrets in .env (ignored) and read them with os.environ.

Finish the Python handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.