Python Best Practices¶
Code style, project structure, packaging, and virtual environments.
Code Style¶
Follow PEP 8 (with pragmatism)¶
# Good: descriptive names, consistent style
def calculate_monthly_revenue(transactions: list[dict], month: int) -> float:
"""Calculate total revenue for a given month."""
return sum(
t["amount"]
for t in transactions
if t["date"].month == month and t["type"] == "income"
)
# Bad: cryptic names, inconsistent
def calc(t, m):
s = 0
for x in t:
if x["date"].month == m:
if x["type"] == "income":
s += x["amount"]
return s
Naming Conventions¶
| Type | Convention | Example |
|---|---|---|
| Variables, functions | snake_case | user_name, get_users() |
| Classes | PascalCase | UserService, HTTPClient |
| Constants | UPPER_SNAKE | MAX_RETRIES, API_BASE_URL |
| Private | _leading_underscore | _internal_method() |
| Modules | short_snake_case | user_service.py |
Imports¶
# Standard library
import os
import sys
from pathlib import Path
from typing import Any
# Third-party (blank line separator)
import requests
from fastapi import FastAPI
from pydantic import BaseModel
# Local
from .models import User
from .utils import validate_email
Project Structure¶
Small project (script/utility)¶
Medium project (package)¶
my-project/
├── src/
│ └── my_project/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ ├── services.py
│ └── utils.py
├── tests/
│ ├── conftest.py
│ ├── test_main.py
│ └── test_services.py
├── pyproject.toml
├── README.md
└── .gitignore
Large project (application)¶
my-app/
├── src/
│ └── my_app/
│ ├── __init__.py
│ ├── config.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── middleware.py
│ ├── domain/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ └── services.py
│ ├── infrastructure/
│ │ ├── __init__.py
│ │ ├── database.py
│ │ └── cache.py
│ └── utils/
│ └── __init__.py
├── tests/
│ ├── unit/
│ ├── integration/
│ └── conftest.py
├── scripts/
├── docs/
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── pyproject.toml
├── Makefile
└── README.md
Virtual Environments¶
Always use a virtual environment. Never install packages globally.
# Create
python3 -m venv .venv
# Activate
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # Windows
# Deactivate
deactivate
# Pin your dependencies
pip freeze > requirements.txt
# Or better use pip-compile for deterministic builds
pip install pip-tools
pip-compile requirements.in # Creates requirements.txt with pinned versions
Configuration¶
Use pyproject.toml (modern standard)¶
[project]
name = "my-project"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = [
"fastapi>=0.100.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.4.0",
"mypy>=1.10",
]
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B", "SIM"]
[tool.mypy]
python_version = "3.10"
strict = true
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--strict-markers -v"
Error Handling¶
# Good: Specific exceptions, helpful messages
def get_user(user_id: int) -> User:
try:
user = db.query(User).get(user_id)
except ConnectionError as e:
logger.error(f"Database connection failed: {e}")
raise ServiceUnavailableError("Cannot reach database") from e
if user is None:
raise UserNotFoundError(f"User {user_id} does not exist")
return user
# Bad: Bare except, silencing errors
def get_user(user_id):
try:
return db.query(User).get(user_id)
except:
return None # Hides bugs!
Logging (not print)¶
import logging
logger = logging.getLogger(__name__)
# Good
logger.info("Processing order", extra={"order_id": order.id, "total": order.total})
logger.error("Payment failed", exc_info=True)
# Bad
print(f"Processing order {order.id}") # Lost in production
Type Hints¶
Use type hints everywhere. They're documentation and catch bugs.
# Good: Clear contract
def process_payment(
amount: float,
currency: str = "INR",
*,
idempotency_key: str | None = None,
) -> PaymentResult:
...
# Catch bugs at development time, not in production
# Run: mypy src/ --strict
Security Basics¶
- Never hardcode secrets use environment variables
- Validate all input use Pydantic models
- Use parameterized queries never f-string SQL
- Pin dependencies avoid supply chain attacks
- Keep dependencies updated
pip auditfor vulnerabilities
# Good
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# Bad (SQL injection!)
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
Performance Tips¶
- Profile before optimizing
python -m cProfile script.py - Use generators for large data
yieldinstead of building lists - Use
__slots__for many instances saves memory - Prefer list comprehensions faster than loops + append
- Use appropriate data structures
setfor membership,dequefor queues - Consider Polars when Pandas is too slow
Git Workflow¶
# Branch naming
feature/add-user-api
fix/login-timeout
chore/update-dependencies
# Commit messages (conventional commits)
feat: add user registration endpoint
fix: resolve timeout in payment processing
docs: update API documentation
refactor: extract email validation to utility
test: add unit tests for order service