Cybersecurity

Build a Production-Style Python CI Pipeline

Python Docker RAG Encryption Cryptography SQL Injection XSS CI/CD Git GitHub Databases SQL Rust Data Science Regression Passwords Hashing
1,164 words Includes Code
Key Takeaway: A production CI pipeline goes beyond running tests. It combines pytest for correctness, linting for code quality, security scanning for vulnerabilities, and dependency auditing for supply-chain safety — catching problems across four dimensions before code reaches production.

Build a Production-Style Python CI Pipeline

Running pytest is a great start. But production software needs more than functional tests. A production-style CI pipeline adds linting, security scanning, and dependency auditing — four layers of protection that catch issues no single tool can find.

This tutorial builds a complete four-stage pipeline using only free, open-source tools. Every stage is explained with practical examples and tested locally.

Production CI pipeline showing four stages: tests, linting, security scanning and dependency audit

Why Four Stages?

Each stage catches a different category of problem:

StageWhat It CatchesExample Issue
① TestsBroken logic, regressionsdivide(10, 0) crashes
② LintingStyle issues, bad patterns!= None instead of is not None
③ SecuritySecrets, weak crypto, injectionHardcoded API key in source
④ DependenciesCVEs, outdated packagesrequests 2.28.0 has known CVE

A test can pass while the code contains a hardcoded secret. Linting can pass while dependencies have known vulnerabilities. You need all four.

CI pipeline detail showing pytest, linting, security scanning, dependency audit stages with decision gate

Stage 1: Unit Tests with pytest

pytest is the standard testing framework for Python. It discovers tests automatically, provides clear failure output, and generates machine-readable reports.

# tests/test_app.py
import pytest
from src.app import compute_hash, process_data, divide

class TestDivide:
    def test_basic(self):
        assert divide(10, 2) == 5.0

    def test_float_result(self):
        assert divide(1, 3) == pytest.approx(0.333, abs=0.01)

    def test_zero_division(self):
        with pytest.raises(ZeroDivisionError):
            divide(10, 0)

Key pytest options for CI:

# Run with verbose output + JUnit XML for GitHub Actions
pytest tests/ --tb=short --verbose --junitxml=report.xml

# Add coverage reporting
pytest tests/ --cov=src --cov-report=xml:coverage.xml
Why JUnit XML? GitHub Actions, GitLab CI, and most CI platforms can parse JUnit XML to show test annotations directly on your commits and pull requests.

Stage 2: Linting and Code Quality

Linting catches issues that tests miss: style violations, unused imports, type errors, and anti-patterns. For modern Python, ruff is the recommended tool — it's 10-100x faster than flake8 and covers both linting and formatting.

# ruff — fast Python linter (replaces flake8 + isort + more)
ruff check src/ --select E,W,S --output-format=github

# Check formatting
ruff format --check src/

# Type checking with mypy
mypy src/ --ignore-missing-imports

Common linting issues in student code:

RuleBadGood
E711!= Noneis not None
E712== Trueif flag:
F841x = compute(); returnRemove unused variable
S324hashlib.md5()hashlib.sha256()

Stage 3: Security Scanning

Security scanning catches vulnerabilities that neither tests nor linting detect: hardcoded secrets, weak cryptography, unsafe deserialization, and injection risks.

# bandit — Python security linter
bandit -r src/ -f json -o bandit-report.json

# detect-secrets — find hardcoded secrets
detect-secrets scan src/ --all-files

# gitleaks — scan git history for secrets
gitleaks detect --source . --report-format json

What security scanning catches:

# Example: hardcoded secret (security scan catches this)
API_KEY = "sk-demo-1234567890abcdef"   # ← HIGH severity

# Example: weak hashing (security scan catches this)
hashlib.md5(data.encode())             # ← MEDIUM severity

# Example: unsafe deserialization (security scan catches this)
pickle.loads(untrusted_data)           # ← HIGH severity
Warning: Security scanners produce false positives. Review each finding before dismissing or fixing. A "HIGH" finding in a test file may be acceptable; the same finding in production code is not.

Stage 4: Dependency Auditing

Your code can be perfect while your dependencies contain known CVEs. Dependency auditing checks every installed package against vulnerability databases.

# pip-audit — check for known vulnerabilities
pip-audit --require-hashes --desc

# safety — alternative vulnerability scanner
safety check --json

# Check for outdated packages
pip list --outdated --format=json

Real example output:

$ pip-audit
Name    Version  ID            Fix
------  -------  ------------  --------
requests  2.28.0  PYSEC-2023-74  Upgrade to 2.31.0
urllib3   1.26.5  PYSEC-2023-212 Upgrade to 2.0.7

Found 2 known vulnerabilities in 1 package.

The Complete GitHub Actions Workflow

Here's the full production pipeline in a single YAML file:

name: Production Python CI

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

jobs:
  test:
    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 tests/ --tb=short -v --junitxml=report.xml
        name: Run tests
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: test-report, path: report.xml }

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12", cache: pip }
      - run: pip install ruff mypy
      - run: ruff check src/ --select E,W,S
        name: Lint with ruff
      - run: mypy src/ --ignore-missing-imports
        name: Type check

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12", cache: pip }
      - run: pip install bandit
      - run: bandit -r src/ -f json -o bandit.json
        name: Security scan
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: security-report, path: bandit.json }

  dependencies:
    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: pip install pip-audit
      - run: pip-audit --desc
        name: Audit dependencies
Student Tip: These 4 jobs run in parallel — the entire pipeline finishes in the time of the slowest job, not the sum of all jobs.

Local Pipeline Demo

Before pushing to GitHub, run the pipeline locally. Here's what each stage produces:

StageCommandDemo Result
Testspytest tests/ -v9/9 passed
Lintingruff check src/3 issues found
Securitybandit -r src/2 vulnerabilities (1 HIGH)
Depspip-audit0 CVEs found

The pipeline correctly caught a hardcoded API key (HIGH) and MD5 usage (MEDIUM) — issues that no test would ever detect.

Project Structure

my-project/
├── .github/
│   └── workflows/
│       └── ci.yml              ← 4-stage pipeline
├── src/
│   ├── __init__.py
│   └── app.py                  ← Your code
├── tests/
│   ├── __init__.py
│   └── test_app.py             ← pytest tests
├── requirements.txt            ← Dependencies
├── pyproject.toml              ← Tool config (ruff, mypy, bandit)
├── .bandit                     ← Bandit config (optional)
└── README.md

Configuring Tools with pyproject.toml

# pyproject.toml — central config for all tools

[tool.ruff]
line-length = 88
select = ["E", "W", "S", "F"]

[tool.ruff.per-file-ignores]
"tests/*" = ["S101"]  # Allow assert in tests

[tool.mypy]
ignore_missing_imports = true
strict = false

[tool.bandit]
exclude_dirs = ["tests"]
skips = ["B101"]  # Allow assert in production code

When Each Stage Matters Most

ScenarioMost Critical Stage
Data Science projectTests — verify calculations
Team project with shared codeLinting — consistent style
Web app handling user dataSecurity — no secrets in code
Production deploymentDependencies — no known CVEs
Student learning projectAll four — build good habits early

Tools Comparison

StageFast / RecommendedAlternative
Testspytestunittest
Lintingruffflake8, pylint
Formattingruff formatblack
Type checkingmypypyright
Securitybanditsemgrep
Secretsgitleaksdetect-secrets
Dependenciespip-auditsafety

Try It Yourself — BestWordz Tools

Practice with these interactive BestWordz tools:

Related BestWordz Articles

CI Pipeline Checklist for Students










Summary

A production-style CI pipeline runs four stages in parallel on every push:

  1. pytest — Verifies your code does what it's supposed to do
  2. Linting — Catches style issues, bad patterns, and unused code
  3. Security scanning — Finds hardcoded secrets, weak crypto, injection risks
  4. Dependency auditing — Checks every package against known CVE databases

All four are free. All four run in parallel. All four catch issues the others miss. Adding them to your project takes under 30 lines of YAML — and the habit of using them is worth more than any single tool.

Further Reading

Discuss this topic on BestWordz Community — Share your CI pipeline setups, troubleshoot workflow issues, and learn from other developers building production-quality automated testing.

Try the JSON Formatter

Put what you've learned into practice with this free BestWordz tool.

Open Tool →

💬 Discuss on BestWordz Community

Join the conversation about Python, Docker, RAG on the BestWordz Community forum.

Visit Forum →