Shipping Python APIs/
Lesson

When you ask AI to "set up CI for my FastAPI project," you get a workflow file in seconds. It will have the right shape, triggers, jobs, steps, but it will be missing the details that make CI actually useful. This lesson teaches you to read those workflows critically, spot what AI leaves out, and understand the infrastructure your pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. runs on.

Workflow files and where they live

Every GitHub Actions workflow is a YAMLWhat is yaml?A human-readable text format used for configuration files, including Docker Compose and GitHub Actions workflows. file inside .github/workflows/. GitHub scans this directory automatically, drop a valid YAML file in there, push it, and the pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. runs. No configuration UI, no third-party setup.

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: "pip"
      - run: pip install -r requirements.txt
      - run: pytest

This is the skeleton that AI generates. It works, but it is the bare minimum. Let's understand each piece and what is missing.

02

Trigger events

The on block defines when the workflow runs. The two most common triggers for CI are push (code lands on a branch) and pull_request (a PR is opened or updated).

yaml
on:
  push:
    branches: [main]         # Runs when code is pushed to main
  pull_request:
    branches: [main]         # Runs when a PR targets main

You can also trigger on schedules (nightly builds), tags (release builds), or manual dispatch (one-off runs from the GitHub UI):

yaml
on:
  schedule:
    - cron: "0 6 * * 1"     # Every Monday at 6 AM UTC
  workflow_dispatch:          # Manual trigger button in GitHub UI
AI pitfall
AI almost always generates only push and pull_request triggers. For Python projects that depend on external packages, a weekly scheduled run catches upstream dependency breakage before it hits your developers on Monday morning.
03

Python setup

The actions/setup-python@v5 action installs a specific Python version on the runner. This is where AI makes its first common mistake: hardcoding a single version or, worse, omitting the version entirely.

yaml
# What AI generates - works, but fragile
- uses: actions/setup-python@v5
  with:
    python-version: "3.11"

# What production needs - explicit version, pinned action
- uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: "pip"

The cache: "pip" option is built into setup-python@v5. It caches the pip download cache based on your requirements.txt hash, so subsequent runs skip downloading packages that have not changed.

Why the Python version matters

Python 3.11, 3.12, and 3.13 have meaningful differences, new syntax features, performance improvements, and deprecated APIs. If your CI runs on 3.11 but your production server runs 3.12, you might miss deprecationWhat is deprecation?Marking a feature or API version as outdated and scheduled for removal, giving users time to switch to the replacement. warnings or use features that do not exist in your CI environment.

Python versionKey changes relevant to CI
3.11Exception groups, tomllib in stdlib
3.12Improved error messages, type statement, f-string improvements
3.13Experimental free-threaded mode, improved typing module
AI pitfall
AI frequently hardcodes Python 3.11 regardless of what your pyproject.toml specifies. Always match your CI Python version to your production Python version, or better yet, use a matrix to test both.
04

Pip caching

Without caching, every CI run downloads every dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. from PyPI from scratch. For a typical FastAPI project with 40-60 transitive dependencies, that is 30-90 seconds of pure download time on every push.

There are two caching approaches:

yaml
# Option 1: Built-in cache in setup-python (simpler)
- uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: "pip"

# Option 2: Explicit cache action (more control)
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

Option 1 is simpler and covers most cases. Option 2 gives you control over the cache key, which matters when you have multiple requirements files (requirements.txt, requirements-dev.txt, requirements-test.txt).

AI pitfall
AI-generated workflows almost never include pip caching. On a project with heavy dependencies like numpy or pandas, this turns a 15-second cached install into a 2-minute download on every single push.
05

Running linting, type checking, and tests

A complete CI pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. for Python runs three categories of checks, in order from fastest to slowest:

yaml
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-python@v5
    with:
      python-version: "3.12"
      cache: "pip"

  - name: Install dependencies
    run: |
      pip install -r requirements.txt
      pip install -r requirements-dev.txt

  - name: Lint with ruff
    run: ruff check .

  - name: Type check with mypy
    run: mypy src/

  - name: Run tests
    run: pytest
StepToolWhat it catchesSpeed
LintruffStyle issues, unused imports, common bugsSeconds
Type checkmypyType mismatches, missing attributes, wrong return typesSeconds to minutes
TestpytestLogic errors, regressions, broken integrationsSeconds to minutes

The order matters. Linting is near-instant and catches obvious issues first. Type checking runs next and surfaces deeper problems. Tests run last because they are the slowest. If linting fails, there is no point waiting for tests.

Separate requirements files

Production dependencies and development dependencies should live in separate files:

requirements.txt          # Production: fastapi, uvicorn, sqlalchemy, etc.
requirements-dev.txt      # Development: pytest, ruff, mypy, etc.

In CI, you install both. In production DockerWhat is docker?A tool that packages your application and all its dependencies into a portable container that runs identically on any machine. images, you install only requirements.txt. AI-generated workflows typically reference only requirements.txt, which means your linterWhat is linter?A tool that scans your code for style violations, common mistakes, and suspicious patterns without running it., type checker, and test runner are not installed, and the CI silently skips those steps or fails with a "command not found" error.

AI pitfall
AI generates pip install -r requirements.txt and then runs pytest, but pytest is a dev dependency. The workflow either fails because pytest is not installed, or AI puts pytest in requirements.txt, shipping test dependencies to production.
06

Pinning action versions

Actions are third-party code running in your pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production.. Pinning to a major version (@v5) protects against breaking changes. Pinning to a commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. SHA protects against supply-chain attacks where a compromised maintainer pushes malicious code.

yaml
# Good - pinned to major version
- uses: actions/setup-python@v5

# Better - pinned to commit SHA
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b

# Dangerous - follows the branch, could change any time
- uses: actions/setup-python@main

For most teams, major version pinning (@v5) is the right balance. SHA pinning is worth the effort for high-security environments.

07

Reading AI-generated workflows critically

Here is a checklist for reviewing any AI-generated GitHub Actions workflow for Python:

CheckWhat to look for
Python versionDoes it match your production version?
Pip cachingIs cache: "pip" set, or actions/cache@v4 configured?
Dev dependenciesAre requirements-dev.txt deps installed for lint/test steps?
Action versionsAre actions pinned to @v4/@v5, not @main?
Lint stepIs ruff (or flake8) running before tests?
Type check stepIs mypy included?
Trigger scopeDoes it run on both push and pull_request?
SecretsAre API keys using ${{ secrets.NAME }}, not hardcoded?
08

Quick reference

ElementSyntaxPurpose
Triggeron: push, on: pull_requestWhen the workflow runs
Python setupactions/setup-python@v5Install specific Python version
Pip cachecache: "pip" in setup-pythonSkip redundant downloads
Lintruff check .Fast code quality check
Type checkmypy src/Catch type errors before runtime
TestpytestVerify correctness
Pin action@v5 or @shaLock action version for security