Skip to Content

Quality Gates

Three-level quality gate system for Django projects. Each level builds on the previous — pass Foundation before targeting Production.

Quality Levels

Foundation (Required for All Code)

CheckRequirementTool
Type annotations100% coveragemypy --strict
No Any / raw dict0 instancesgrep + mypy
Pydantic v2 modelsAll data structuresManual review
Custom exceptionsException hierarchyManual review
No mutable defaults0 instances (=[], ={})grep
No global state0 module-level mutable varsgrep

Production (Required for Deployment)

CheckRequirementTool
Test coverage>= 95%pytest --cov-fail-under=95
Function length< 20 linesflake8
Cyclomatic complexity< 10flake8 --max-complexity=10
Async correctnessNo sync ORM in asyncgrep + runtime checks
Resource cleanupContext managers for I/OManual review
Security audit0 findingsbandit
CheckRequirementTool
Performance benchmarksAPI < 200ms p95pytest-benchmark
Memory usage< 100MB per workerpsutil monitoring
Error rate< 1% in productionPrometheus
Documentation100% public APIinterrogate
MonitoringMetrics + alerts configuredPrometheus + Grafana

Django-Specific Zero-Tolerance Violations

These patterns are forbidden in Django projects and must be fixed before merge:

Sync ORM in Async Context

# FORBIDDEN: sync ORM call inside async function async def bad_view(request): user = User.objects.get(id=1) # SynchronousOnlyOperation at runtime # REQUIRED: use a-prefixed method async def good_view(request): user = await User.objects.aget(id=1)

Missing transaction.atomic for Multi-Step Writes

# FORBIDDEN: multiple writes without transaction def bad_transfer(from_id: int, to_id: int, amount: Decimal): Account.objects.filter(id=from_id).update(balance=F("balance") - amount) Account.objects.filter(id=to_id).update(balance=F("balance") + amount) # If second update fails, data is inconsistent # REQUIRED: wrap in transaction from django.db import transaction def good_transfer(from_id: int, to_id: int, amount: Decimal): with transaction.atomic(): Account.objects.filter(id=from_id).update(balance=F("balance") - amount) Account.objects.filter(id=to_id).update(balance=F("balance") + amount)
# FORBIDDEN: N+1 query pattern def bad_list_books(): books = Book.objects.all() for book in books: print(book.author.name) # N extra queries # REQUIRED: eager loading def good_list_books(): books = Book.objects.select_related("author").all() for book in books: print(book.author.name) # 1 query total

Unvalidated User Input in ORM Queries

# FORBIDDEN: raw user input in extra/raw def bad_search(query: str): return Book.objects.extra(where=[f"title LIKE '%{query}%'"]) # REQUIRED: parameterized queries def good_search(query: str): return Book.objects.filter(title__icontains=query)

Automated Checks

Pre-commit Configuration

# Type checking (Foundation) mypy . --strict --show-error-codes # Code quality (Foundation + Production) black . --check isort . --check-only flake8 . --max-complexity=10 --max-line-length=88 # Security (Production) bandit -r src/ -f json # Test coverage (Production) pytest --cov=src --cov-fail-under=95 --maxfail=1

Django-Specific grep Checks

# Detect sync ORM in async functions (zero-tolerance) # Look for .objects. calls without await/a-prefix in async def blocks grep -rn "async def" src/ | while read line; do file=$(echo "$line" | cut -d: -f1) grep -n "\.objects\.\(get\|filter\|create\|update\|delete\|all\|count\|exists\)(" "$file" \ | grep -v "await" | grep -v "\.a\(get\|filter\|create\|update\|delete\|all\|count\|exists\)(" done # Detect missing transaction.atomic for bulk writes grep -rn "\.update(" src/ | grep -v "transaction" | grep -v "test" # Detect N+1 candidates: all() without select/prefetch grep -rn "\.objects\.all()" src/ | grep -v "select_related\|prefetch_related" # Pydantic v1 leftovers grep -rn "\.parse_obj\|\.dict()\|@validator\b" src/

CI Pipeline

# .github/workflows/quality.yml name: Quality Gates on: [push, pull_request] jobs: foundation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: { python-version: "3.12" } - run: pip install -e .[dev] - run: mypy . --strict - run: black --check . - run: isort --check-only . production: needs: foundation runs-on: ubuntu-latest services: postgres: image: postgres:16 env: { POSTGRES_DB: test, POSTGRES_PASSWORD: test } ports: ["5432:5432"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: { python-version: "3.12" } - run: pip install -e .[dev] - run: pytest --cov=src --cov-fail-under=95 - run: bandit -r src/ - run: flake8 . --max-complexity=10

Prometheus Monitoring

Instrument Django middleware to collect metrics for Enterprise-level monitoring.

Metrics Middleware

import time from prometheus_client import Counter, Histogram, Gauge REQUEST_COUNT = Counter( "django_http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"], ) REQUEST_DURATION = Histogram( "django_http_request_duration_seconds", "HTTP request duration", ["method", "endpoint"], ) ACTIVE_REQUESTS = Gauge( "django_http_active_requests", "Currently active HTTP requests", ) class PrometheusMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): ACTIVE_REQUESTS.inc() start = time.perf_counter() response = self.get_response(request) duration = time.perf_counter() - start endpoint = self._get_endpoint(request) REQUEST_COUNT.labels( method=request.method, endpoint=endpoint, status=response.status_code, ).inc() REQUEST_DURATION.labels( method=request.method, endpoint=endpoint, ).observe(duration) ACTIVE_REQUESTS.dec() return response def _get_endpoint(self, request) -> str: if request.resolver_match: return request.resolver_match.route return "unknown"

Async Middleware Variant

class AsyncPrometheusMiddleware: def __init__(self, get_response): self.get_response = get_response async def __call__(self, request): ACTIVE_REQUESTS.inc() start = time.perf_counter() response = await self.get_response(request) duration = time.perf_counter() - start endpoint = self._get_endpoint(request) REQUEST_COUNT.labels( method=request.method, endpoint=endpoint, status=response.status_code, ).inc() REQUEST_DURATION.labels( method=request.method, endpoint=endpoint, ).observe(duration) ACTIVE_REQUESTS.dec() return response

Register in settings:

MIDDLEWARE = [ "project.middleware.PrometheusMiddleware", # First — to capture all requests # ... other middleware ]

Code Review Checklist

Before approving a Django PR, verify:

  • No sync ORM calls in async def functions
  • Multi-step writes wrapped in transaction.atomic()
  • QuerySets use select_related / prefetch_related where needed
  • No raw SQL — use ORM or parameterized cursor.execute()
  • Async views use a-prefixed ORM methods
  • WebSocket consumers clean up groups in disconnect()
  • settings.py has no hardcoded secrets
  • Migrations are included and reversible
  • New endpoints have corresponding tests
  • mypy --strict passes with no new errors

Rules

  1. Pass Foundation checks before deploying any code to staging
  2. Pass Production checks before deploying to production
  3. Fix zero-tolerance violations (sync ORM in async, missing transactions) immediately — no exceptions
  4. Run Django-specific grep checks in CI alongside standard linting
  5. Add Prometheus middleware early — retrofitting monitoring is harder than starting with it
  6. Review checklist items are blocking — any unchecked item blocks merge
Last updated on