Python
Overview
Python is the dominant language for backend services, data engineering, and scripting. This reference covers package management (with a focus on the modern uv toolchain), FastAPI project patterns, bare-script conventions, and production deployment practices.
Package management
uv (modern, recommended)
uv is a fast Python package and project manager written in Rust. It replaces pip, pip-tools, virtualenv, and poetry.
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create a new project
uv init myproject
cd myproject
# Add dependencies
uv add fastapi uvicorn[standard] sqlalchemy
# Add dev dependencies
uv add --dev pytest ruff mypy
# Sync environment (install all deps from lockfile)
uv sync
# Run a command in the project environment
uv run python main.py
uv run pytest
uv run ruff check .
# Run a one-off script with dependencies
uv run --with httpx python -c "import httpx; print(httpx.get('https://api.example.com').json())"
# Update all packages
uv lock --upgrade
# Export to requirements.txt
uv export --format requirements-txt > requirements.txt
pip + venv (traditional)
pip + venv is the standard Python toolchain that ships with every interpreter — no extra install needed. It's slower and simpler than uv, but it's what most CI images, system packages, and tutorials assume, so it's the right choice when you can't install extra tooling.
# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# Install packages
pip install fastapi uvicorn[standard]
pip install -r requirements.txt
pip freeze > requirements.txt # Pin current versions
# Exit venv
deactivate
Package tool comparison
| Tool | Speed | Lockfile | Workspace | Use case |
|---|---|---|---|---|
uv | Very fast | uv.lock | Monorepo support | Modern projects, CI |
pip + venv | Slow | requirements.txt (manual pin) | Manual | Simple projects |
poetry | Moderate | poetry.lock | Workspace support | Library packaging |
pipenv | Slow | Pipfile.lock | Limited | Legacy projects |
FastAPI patterns
Minimal server
A FastAPI application is just a Python module exposing an app instance. The lifespan context manager runs once on startup (connect to the database, load models) and again on shutdown (close connections, flush buffers), keeping setup out of individual route handlers.
# main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: connect to database, load models, etc.
print("Starting up...")
yield
# Shutdown: close connections, flush buffers, etc.
print("Shutting down...")
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def root():
return {"message": "Hello, World"}
@app.get("/health")
async def health():
return {"status": "ok"}
uv run uvicorn main:app --host 0.0.0.0 --port 8000 --reload
Structured routes
Once you have more than a few endpoints, split routes into modules using APIRouter. A router groups related paths under a shared prefix and tags, keeping main.py minimal and your generated OpenAPI docs (at /docs) neatly organized.
# routes/users.py
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/users", tags=["users"])
class UserCreate(BaseModel):
name: str
email: str
class UserResponse(BaseModel):
id: int
name: str
email: str
@router.post("/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
# Persist the user — input is already validated by Pydantic
return {"id": 1, "name": user.name, "email": user.email}
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
if user_id != 1:
raise HTTPException(status_code=404, detail="User not found")
return {"id": user_id, "name": "John", "email": "john@example.com"}
Dependency injection
FastAPI's Depends declares reusable building blocks — API-key checks, database sessions, settings — that FastAPI resolves automatically when a route runs. Dependencies written as async generators also get deterministic cleanup: the finally block runs after the response, which is where you close a connection.
# deps.py
from typing import Annotated
from fastapi import Depends, Header, HTTPException
async def get_api_key(x_api_key: Annotated[str, Header()]) -> str:
if x_api_key != "expected-secret":
raise HTTPException(status_code=403, detail="Invalid API key")
return x_api_key
async def get_db():
# In production: return an async database session
db = {"connected": True}
try:
yield db
finally:
pass # Close the DB connection here
# Usage in route
@app.get("/secure-data")
async def secure_endpoint(
api_key: Annotated[str, Depends(get_api_key)],
db: Annotated[dict, Depends(get_db)],
):
return {"data": "sensitive", "db": db}
Pydantic validation
Request bodies and response models in FastAPI are Pydantic models, so validation happens at the boundary before your code runs. Field constraints describe ranges, lengths, and formats, while field_validators handle cross-field logic — invalid input is rejected automatically with a 422 response.
from pydantic import BaseModel, Field, field_validator
from typing import Optional
from datetime import datetime
class OrderCreate(BaseModel):
product_id: int = Field(gt=0, description="Product identifier")
quantity: int = Field(ge=1, le=100, description="Items to order")
email: str = Field(pattern=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
notes: Optional[str] = Field(default=None, max_length=500)
@field_validator("quantity")
@classmethod
def validate_quantity(cls, v: int) -> int:
if v > 50:
raise ValueError("Large orders require approval")
return v
Background tasks
BackgroundTasks runs work after the response has been sent, so slow side effects like confirmation emails or notifications don't hold up the request. It's the right tool for fire-and-forget work; for heavier or retryable jobs, hand off to a proper queue (Celery, RQ, or similar).
from fastapi import BackgroundTasks
def send_confirmation_email(email: str, order_id: int):
# Simulate sending email (use a real email service in production)
print(f"Sending confirmation to {email} for order {order_id}")
@app.post("/orders")
async def create_order(
order: OrderCreate,
background_tasks: BackgroundTasks,
):
order_id = 42 # Persist the order first
background_tasks.add_task(send_confirmation_email, order.email, order_id)
return {"order_id": order_id, "status": "pending"}
Middleware
HTTP middleware wraps every request and response that passes through the app. It's the place for cross-cutting concerns — timing headers, CORS, request logging, rate limiting — that should apply uniformly to all routes.
from fastapi import Request
import time
@app.middleware("http")
async def add_timing_header(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
response.headers["X-Process-Time"] = str(elapsed)
return response
Bare Python scripts
Shebang and safeties
The #!/usr/bin/env python3 shebang makes a script directly executable (./myscript.py), and the if __name__ == "__main__" guard means the same file can also be imported as a module without running anything. Routing through a main() function with argument checking keeps the logic testable and gives users a clear usage message.
#!/usr/bin/env python3
"""myscript.py — one-line description of what this does."""
import sys
def main() -> None:
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <input-file>", file=sys.stderr)
sys.exit(1)
input_file = sys.argv[1]
# ... do work ...
if __name__ == "__main__":
main()
Common recipe: read JSON/CSV
The standard library covers most file-format needs without any dependencies. json plus pathlib gives one-line read/write of config-style files, and csv.DictReader converts rows into dicts keyed by the header row.
import json
import csv
from pathlib import Path
# Read JSON
data = json.loads(Path("config.json").read_text())
# Write JSON
Path("output.json").write_text(json.dumps(data, indent=2))
# Read CSV
with open("data.csv") as f:
reader = csv.DictReader(f)
rows = list(reader)
Common recipe: subprocess commands
subprocess.run is the safe way to run an external command and capture its output — always pass a list of arguments, never a shell string. check=True raises on a non-zero exit, and the Popen pipe pattern chains two processes together like a shell pipeline.
import subprocess
result = subprocess.run(
["git", "log", "--oneline", "-5"],
capture_output=True,
text=True,
check=True,
)
print(result.stdout)
# Pipe: chain two processes like a shell pipeline
ls = subprocess.Popen(["ls", "/etc"], stdout=subprocess.PIPE)
grep = subprocess.run(
["grep", "host"],
stdin=ls.stdout,
capture_output=True,
text=True,
)
Common recipe: HTTP client
httpx is the modern HTTP client with a requests-style API plus full async support. Using an AsyncClient inside a context manager pools connections and guarantees they're released after the request.
import httpx
async def fetch_data(url: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10.0)
response.raise_for_status()
return response.json()
Common project structure
This layout follows the src-layout convention: the importable package lives under src/, separated from tests and tooling, so you never accidentally import from the repo root. Routes, models, and dependencies each get their own module, keeping every file small and focused.
myproject/
├── pyproject.toml # Project metadata and dependencies
├── uv.lock # Lockfile (generated)
├── src/
│ └── myproject/
│ ├── __init__.py
│ ├── main.py # Application entrypoint
│ ├── config.py # Settings / environment
│ ├── routes/
│ │ ├── __init__.py
│ │ └── users.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ └── deps.py # Dependencies (DB, auth)
├── tests/
│ ├── __init__.py
│ └── test_users.py
├── Dockerfile
└── README.md
Production deployment
Dockerfile
This Dockerfile builds a slim production image: dependencies are installed into an image layer from the lockfile (uv sync --frozen), so the build is reproducible and dev packages are excluded. Only the source is copied afterward, and the container runs uvicorn directly.
FROM python:3.12-slim
WORKDIR /app
# Install uv
COPY /uv /usr/local/bin/uv
# Copy project files
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
# Copy source
COPY src/ ./src/
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "src.myproject.main:app", "--host", "0.0.0.0", "--port", "8000"]
systemd service unit
systemd runs the app as a managed service: it restarts on failure with a 5-second backoff and runs under a dedicated non-root user. Drop this unit into /etc/systemd/system/, then enable it with systemctl enable --now myapp.
[Unit]
Description=My FastAPI Application
After=network.target
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myproject
ExecStart=/usr/local/bin/uv run uvicorn src.myproject.main:app --host 0.0.0.0 --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Environment configuration
pydantic-settings maps environment variables onto typed settings with defaults and reads .env files automatically. Required fields without defaults (here secret_key) fail fast at startup with a clear error, so a misconfigured app never runs half-initialized.
# config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "postgresql://localhost:5432/mydb"
redis_url: str = "redis://localhost:6379"
secret_key: str
environment: str = "development"
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
settings = Settings()
See also
- Rust Reference — systems programming with cargo and axum
- Node.js & TypeScript — JavaScript/TypeScript runtime reference