Python in Production: Architecture, Performance and Best Practices for 2026¶
Python dominates in AI/ML, data engineering, automation and backend development. But moving from a Jupyter notebook to production requires discipline. This guide summarises proven approaches for 2026.
Production Python Project Architecture¶
my-project/
├── src/
│ └── myproject/
│ ├── __init__.py # Version, public API
│ ├── core/ # Business logic (pure Python)
│ │ ├── models.py # Dataclasses, Pydantic models
│ │ ├── engine.py # Core computation logic
│ │ └── validators.py # Validation, constraints
│ ├── adapters/ # I/O (DB, API, files)
│ │ ├── database.py
│ │ ├── api_client.py
│ │ └── file_handler.py
│ ├── services/ # Orchestration (links core + adapters)
│ │ └── pipeline.py
│ └── cli/ # CLI interface
│ └── main.py
├── tests/
│ ├── unit/ # Fast, isolated
│ ├── integration/ # With external dependencies
│ └── conftest.py
├── pyproject.toml # Single configuration file
├── Dockerfile
├── Makefile
└── README.md
Key Principles¶
- Separate I/O from logic —
core/has no imports from external services - Dependency injection — adapters are passed as parameters, not global imports
- Dataclasses everywhere — typed data structures instead of dicts
- Single entry point — CLI or API, never both in one module
Typing — Necessity, Not Luxury¶
Python 3.12+ brings a mature type system. In 2026, untyped code does not belong in production.
from dataclasses import dataclass
from typing import Optional, Protocol
from collections.abc import Sequence
# Dataclass instead of dict
@dataclass(frozen=True, slots=True)
class TradeSignal:
symbol: str
direction: float # -1.0 to 1.0
strength: float # 0.0 to 1.0
strategy: str
timestamp: str
def __post_init__(self):
if not -1 <= self.direction <= 1:
raise ValueError(f"direction must be [-1, 1], got {self.direction}")
if not 0 <= self.strength <= 1:
raise ValueError(f"strength must be [0, 1], got {self.strength}")
# Protocol instead of ABC (structural typing)
class DataFetcher(Protocol):
def fetch_ohlcv(self, symbol: str, timeframe: str, limit: int) -> list[dict]: ...
def fetch_ticker(self, symbol: str) -> dict: ...
# Generic functions with precise types
def combine_signals(
signals: Sequence[TradeSignal],
weights: Optional[dict[str, float]] = None,
) -> TradeSignal:
"""Combines signals from multiple strategies."""
if not signals:
raise ValueError("No signals to combine")
total_weight = 0.0
weighted_dir = 0.0
for sig in signals:
w = (weights or {}).get(sig.strategy, 1.0)
weighted_dir += sig.direction * sig.strength * w
total_weight += w
avg_direction = weighted_dir / total_weight if total_weight > 0 else 0.0
return TradeSignal(
symbol=signals[0].symbol,
direction=max(-1.0, min(1.0, avg_direction)),
strength=abs(avg_direction),
strategy="ensemble",
timestamp=signals[0].timestamp,
)
Typing Toolchain¶
| Tool | Purpose | Recommendation |
|---|---|---|
| mypy | Static type checker | --strict mode in CI |
| pyright | Faster alternative | VS Code integration |
| Pydantic v2 | Runtime validation + serialisation | For API models |
| beartype | Runtime type checking (decorator) | For critical paths |
# pyproject.toml — mypy strict mode
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
Performance — Python Doesn’t Have to Be Slow¶
NumPy/Pandas Vectorisation¶
# ❌ Slow — Python loop
def slow_moving_average(prices: list[float], window: int) -> list[float]:
result = []
for i in range(len(prices)):
if i < window:
result.append(float('nan'))
else:
result.append(sum(prices[i-window:i]) / window)
return result
# ✅ Fast — NumPy vectorisation (100–1000× faster)
import numpy as np
def fast_moving_average(prices: np.ndarray, window: int) -> np.ndarray:
cumsum = np.cumsum(np.insert(prices, 0, 0))
return (cumsum[window:] - cumsum[:-window]) / window
Profiling and Optimisation¶
# cProfile to identify bottlenecks
python -m cProfile -s cumtime my_script.py
# line_profiler for line-level analysis
kernprof -l -v my_script.py
# memory_profiler for memory leaks
python -m memory_profiler my_script.py
When to Reach for C Extensions¶
| Situation | Solution |
|---|---|
| Numerical computation | NumPy, SciPy (already in C) |
| Custom hot loop | numba JIT compilation |
| Data transformations | Polars (Rust backend) |
| Parallelism | multiprocessing, concurrent.futures |
| I/O bound | asyncio, httpx |
| Truly critical | Cython, PyO3 (Rust) |
# Numba — JIT compilation for numerical code
from numba import njit
@njit
def fast_ewma(data: np.ndarray, alpha: float) -> np.ndarray:
result = np.empty_like(data)
result[0] = data[0]
for i in range(1, len(data)):
result[i] = alpha * data[i] + (1 - alpha) * result[i-1]
return result
Testing — Pyramid, Not Iceberg¶
/\
/ \ E2E (5%)
/ \
/ Integ \ Integration (15%)
/ ration \
/____________\
/ Unit Tests \ Unit (80%)
/________________\
# conftest.py — fixtures for dependency injection
import pytest
from unittest.mock import AsyncMock
@pytest.fixture
def mock_fetcher():
fetcher = AsyncMock()
fetcher.fetch_ohlcv.return_value = [
{"open": 100, "high": 105, "low": 98, "close": 103, "volume": 1000}
]
return fetcher
@pytest.fixture
def engine(mock_fetcher):
from myproject.core.engine import TradingEngine
return TradingEngine(fetcher=mock_fetcher)
# test_engine.py — clean unit tests
def test_signal_generation(engine):
signal = engine.generate_signal("BTC/USDT")
assert -1 <= signal.direction <= 1
assert 0 <= signal.strength <= 1
# Parametrized tests
@pytest.mark.parametrize("direction,expected", [
(0.8, "long"),
(-0.8, "short"),
(0.1, "flat"),
])
def test_signal_action(direction, expected):
assert signal_to_action(direction, threshold=0.3) == expected
CI Pipeline¶
# GitHub Actions
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e ".[dev]"
- run: mypy src/ --strict
- run: ruff check src/ tests/
- run: pytest tests/ -v --cov=src --cov-report=xml
- run: pytest tests/integration/ -v -m integration
Packaging — pyproject.toml Is the Standard¶
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-trading-framework"
version = "0.3.0"
description = "Quantitative trading framework"
requires-python = ">=3.11"
dependencies = [
"numpy>=1.26",
"pandas>=2.1",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"mypy>=1.8",
"ruff>=0.2",
"pytest-cov",
]
exchange = [
"ccxt>=4.0",
]
[tool.ruff]
target-version = "py312"
line-length = 100
select = ["E", "F", "I", "UP", "B", "SIM"]
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"integration: marks tests requiring external services",
]
Deployment¶
Docker Multi-Stage Build¶
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir build && \
pip wheel --no-cache-dir --wheel-dir /wheels -e ".[exchange]"
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*.whl && rm -rf /wheels
COPY src/ src/
USER nobody
CMD ["python", "-m", "myproject.cli.main"]
Production Monitoring¶
import structlog
import time
from functools import wraps
logger = structlog.get_logger()
def monitor(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = func(*args, **kwargs)
duration = time.perf_counter() - start
logger.info("function_completed",
function=func.__name__,
duration_ms=round(duration * 1000, 2),
)
return result
except Exception as e:
duration = time.perf_counter() - start
logger.error("function_failed",
function=func.__name__,
error=str(e),
duration_ms=round(duration * 1000, 2),
)
raise
return wrapper
Conclusion¶
Production Python in 2026 requires:
- Strict typing — mypy strict in CI, dataclasses everywhere
- Clean architecture — separate I/O from logic
- Vectorisation — NumPy/Polars for numerical operations
- Quality automation — ruff + mypy + pytest on every commit
- Containerisation — multi-stage Docker, nobody user
Python remains the dominant language for data and AI. With discipline, it’s ready for mission-critical production systems too.
Need help with your Python application architecture? Contact us for a consultation.
Need help with implementation?
Our experts can help with design, implementation, and operations. From architecture to production.
Contact us