Build a Production-Style Python CI Pipeline
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.
Why Four Stages?
Each stage catches a different category of problem:
| Stage | What It Catches | Example Issue |
|---|---|---|
| ① Tests | Broken logic, regressions | divide(10, 0) crashes |
| ② Linting | Style issues, bad patterns | != None instead of is not None |
| ③ Security | Secrets, weak crypto, injection | Hardcoded API key in source |
| ④ Dependencies | CVEs, outdated packages | requests 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.
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
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:
| Rule | Bad | Good |
|---|---|---|
| E711 | != None | is not None |
| E712 | == True | if flag: |
| F841 | x = compute(); return | Remove unused variable |
| S324 | hashlib.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
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
Local Pipeline Demo
Before pushing to GitHub, run the pipeline locally. Here's what each stage produces:
| Stage | Command | Demo Result |
|---|---|---|
| Tests | pytest tests/ -v | 9/9 passed |
| Linting | ruff check src/ | 3 issues found |
| Security | bandit -r src/ | 2 vulnerabilities (1 HIGH) |
| Deps | pip-audit | 0 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
| Scenario | Most Critical Stage |
|---|---|
| Data Science project | Tests — verify calculations |
| Team project with shared code | Linting — consistent style |
| Web app handling user data | Security — no secrets in code |
| Production deployment | Dependencies — no known CVEs |
| Student learning project | All four — build good habits early |
Tools Comparison
| Stage | Fast / Recommended | Alternative |
|---|---|---|
| Tests | pytest | unittest |
| Linting | ruff | flake8, pylint |
| Formatting | ruff format | black |
| Type checking | mypy | pyright |
| Security | bandit | semgrep |
| Secrets | gitleaks | detect-secrets |
| Dependencies | pip-audit | safety |
Try It Yourself — BestWordz Tools
Practice with these interactive BestWordz tools:
- Password Strength Checker — Test password security concepts
- JSON Formatter — Validate CI report JSON output
- Secure Random Token Generator — Generate secrets for CI environments
- URL Encoder/Decoder — Handle encoded values in configurations
Related BestWordz Articles
- GitHub Actions Explained: Build Your First CI/CD Pipeline — The foundational CI/CD tutorial
- SQL Injection Explained and Prevented — Security scanning catches injection patterns
- Cross-Site Scripting Explained for Web Developers — Security scanning context
- Secrets Management for Developers — Why hardcoded secrets are dangerous
- Docker Security for Developers — Container security in CI/CD
- Hashing vs Encryption vs Encoding — Understanding weak crypto detection
- <Local Python Docker Container Workspace — Docker-based CI environments
- Protecting API Keys and Secrets — Secret scanning context
CI Pipeline Checklist for Students
Summary
A production-style CI pipeline runs four stages in parallel on every push:
- pytest — Verifies your code does what it's supposed to do
- Linting — Catches style issues, bad patterns, and unused code
- Security scanning — Finds hardcoded secrets, weak crypto, injection risks
- 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
- GitHub Actions: Building and Testing Python
- Ruff Documentation
- Bandit Security Linter Documentation
- <pip-audit Documentation
- pytest Official Documentation
- mypy — Static Type Checker
Try the JSON Formatter
Put what you've learned into practice with this free BestWordz tool.
💬 Discuss this topic
Have questions or insights about Build a Production-Style Python CI Pipeline? Join the BestWordz Community.
📚 Related Articles
Introduction
Computer programming is undergoing its most significant transformation since the invention of high-…
CybersecurityThe 15 AI Security Domains
AI security is not one problem — it is 15 interconnected domains. From prompt injection to sandboxi…
CybersecuritySecrets Management for Developers: From .env Files to Secret Managers
KEY TAKEAWAY Secrets management is the practice of storing, accessing, rotating and revoking cred…
CybersecurityThe 10-Stage CS Learning Roadmap
A computer science education in 2026 requires more than traditional coursework. Today's students ne…
CybersecurityIs AI-Generated Code Secure? A Developer Security Checklist
Key Takeaway AI-generated code is not automatically secure. LLMs produce syntactically …
CybersecurityDocker Security for Developers: 15 Practical Rules
KEY TAKEAWAY Docker containers run with permissive defaults. Run as non-root, use minimal base im…
🔧 Related Tools
Password Strength Checker
Analyze password strength, entropy, and common weaknesses - entirely in your browser.
Try it now →Secure Random Token Generator
Generate cryptographically secure random tokens for API keys, session IDs, and more.
Try it now →URL Encoder
Encode and decode URL data, entirely in your browser.
Try it now →URL Encoder
Percent-encode text for URLs — as a query component or a full URI — right in your browser.
Try it now →💬 Discuss on BestWordz Community
Join the conversation about Python, Docker, RAG on the BestWordz Community forum.
Visit Forum →