Python Coding Standards
Comprehensive standards for writing clean, maintainable Python code following modern best practices.
When to Activate
- Starting a new Python project
- Setting up project structure and tooling
- Reviewing code for standards compliance
- Configuring linters and type checkers
- Onboarding new team members
The Zen of Python
Core principles that guide Python development:
| Principle |
Application |
| Explicit is better than implicit |
Don't hide behavior; make intent clear |
| Simple is better than complex |
Choose straightforward solutions |
| Readability counts |
Code is read more than written |
| Flat is better than nested |
Avoid deep nesting; use early returns |
| Errors should never pass silently |
Handle or propagate exceptions properly |
process(data, True, False, None)
process(data, validate=True, cache=False, timeout=None)
Naming Conventions
Complete Reference
| Element |
Convention |
Good Examples |
Bad Examples |
| Modules |
short, lowercase |
users.py, db.py, config.py |
UserManager.py, data_base.py |
| Packages |
lowercase, no underscores |
mypackage, reportgen |
my_package, ReportGen |
| Classes |
PascalCase, nouns |
UserProfile, ReportGenerator |
userProfile, generate_report |
| Functions |
snake_case, verbs |
get_user(), calculate_total() |
GetUser(), user() |
| Methods |
snake_case, verbs |
process_data(), validate() |
ProcessData(), doIt() |
| Variables |
snake_case, nouns |
user_list, total_count |
userList, x, data |
| Constants |
UPPER_SNAKE_CASE |
MAX_RETRIES, API_TIMEOUT |
max_retries, MaxRetries |
| Protected |
_single_underscore |
_internal_cache, _validate() |
internal_cache |
| Private |
__double_underscore |
__secret_key |
_secret_key |
Naming Guidelines
def get_active_users() -> list[User]: ...
def calculate_discount(price: float) -> float: ...
def validate_email(email: str) -> bool: ...
user_count = len(users)
active_sessions = get_sessions(status="active")
report_config = load_config("report")
for u in users:
process(u)
for user in users:
process(user)
squares = [x * x for x in numbers]
Code Layout
Indentation and Spacing
def process_data(
data: list[str],
*,
timeout: int = 30,
validate: bool = True,
) -> dict[str, Any]:
"""Process the input data.
Args:
data: List of strings to process
timeout: Operation timeout in seconds
validate: Whether to validate input
Returns:
Processed data as dictionary
"""
if validate:
data = [item for item in data if item]
return {"items": data, "count": len(data)}
Line Length
- Maximum: 88 characters (ruff/black default)
- Docstrings/comments: 72 characters preferred
result = some_function(
argument_one,
argument_two,
argument_three,
)
message = (
"This is a very long message that needs to be "
"split across multiple lines for readability."
)
if (
condition_one
and condition_two
and condition_three
):
do_something()
Blank Lines
import os
class UserService:
"""Service for user operations."""
def __init__(self, db: Database):
self.db = db
def get_user(self, user_id: str) -> User | None:
return self.db.find_user(user_id)
def create_user(self, data: UserData) -> User:
return self.db.create_user(data)
def helper_function():
"""Standalone helper function."""
pass
Type Hints (Python 3.12+)
Modern Syntax
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
def find_user(user_id: str) -> User | None:
return users.get(user_id)
def parse_value(value: str) -> int | float | str:
try:
return int(value)
except ValueError:
try:
return float(value)
except ValueError:
return value
When to Use Type Hints
| Scenario |
Type Hints Required? |
| Public function signatures |
Yes |
| Class attributes |
Yes |
| Complex variables |
Yes |
| Simple local variables |
No (inferred) |
| Quick scripts |
No |
| Tests (assertions are implicit types) |
Optional |
Advanced Typing
from typing import TypeVar, Protocol, Generic
from collections.abc import Callable, Iterator
T = TypeVar('T')
def first(items: list[T]) -> T | None:
return items[0] if items else None
class Renderable(Protocol):
def render(self) -> str: ...
def render_all(items: list[Renderable]) -> str:
return "\n".join(item.render() for item in items)
Handler = Callable[[str, int], bool]
def register_handler(handler: Handler) -> None:
pass
JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
Imports
Organization
import os
import sys
from pathlib import Path
from typing import Any
import httpx
from pydantic import BaseModel, Field
from .models import User, Report
from .utils import get_logger
from . import constants
Best Practices
from mypackage.models import User
from .models import User
from ..utils import helper
from os import *
from os.path import join, exists
import os.path
os.path.join(...)
Error Handling
EAFP Pattern
def get_value(data: dict, key: str, default: Any = None) -> Any:
try:
return data[key]
except KeyError:
return default
def get_value(data: dict, key: str, default: Any = None) -> Any:
if key in data:
return data[key]
return default
Exception Handling
try:
result = process(data)
except ValueError as e:
logger.warning("Invalid data: %s", e)
result = default_value
except ConnectionError as e:
logger.error("Connection failed: %s", e)
raise
try:
config = load_config(path)
except FileNotFoundError as e:
raise ConfigError(f"Config not found: {path}") from e
except json.JSONDecodeError as e:
raise ConfigError(f"Invalid JSON in {path}") from e
try:
risky_operation()
except:
pass
try:
risky_operation()
except Exception as e:
logger.exception("Unexpected error")
raise
Custom Exception Hierarchy
class AppError(Exception):
"""Base exception for application errors."""
class ValidationError(AppError):
"""Raised when input validation fails."""
class NotFoundError(AppError):
"""Raised when a resource is not found."""
class ConfigError(AppError):
"""Raised when configuration is invalid."""
def get_user(user_id: str) -> User:
user = db.find_user(user_id)
if not user:
raise NotFoundError(f"User not found: {user_id}")
return user
Logging
Configuration
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
Log Levels
| Level |
When to Use |
Example |
DEBUG |
Development details |
logger.debug("Cache hit for key %s", key) |
INFO |
Normal operations |
logger.info("User %s logged in", user_id) |
WARNING |
Recoverable issues |
logger.warning("Retry %d/%d", attempt, max) |
ERROR |
Errors (no traceback) |
logger.error("Failed to connect: %s", err) |
EXCEPTION |
Errors with traceback |
logger.exception("Unexpected error") |
CRITICAL |
System failures |
logger.critical("Database unavailable") |
Best Practices
logger.info("Processing user %s with %d items", user_id, len(items))
logger.debug(f"Processing user {user_id} with {len(items)} items")
logger.info(
"Request completed",
extra={"user_id": user_id, "duration_ms": duration}
)
try:
process(data)
except Exception:
logger.exception("Failed to process data")
raise
print("User logged in")
logger.info("User logged in")
Structured Logging
import logging
import json
class JSONFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
if hasattr(record, "user_id"):
log_data["user_id"] = record.user_id
return json.dumps(log_data)
Configuration Management
Environment Variables
import os
API_KEY = os.environ["API_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
MAX_CONNECTIONS = int(os.getenv("MAX_CONNECTIONS", "10"))
def validate_config():
"""Validate configuration at application startup."""
if not API_KEY:
raise ValueError("API_KEY is required")
if not DATABASE_URL.startswith(("postgres://", "postgresql://")):
raise ValueError("DATABASE_URL must be a PostgreSQL connection string")
Pydantic Settings
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
"""Application settings loaded from environment."""
api_key: str = Field(..., description="API key for external service")
database_url: str = Field(..., description="Database connection URL")
debug: bool = Field(default=False)
log_level: str = Field(default="INFO")
max_connections: int = Field(default=10, ge=1, le=100)
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
print(settings.api_key)
Project Structure
Recommended Layout
project-root/
├── src/
│ └── mypackage/
│ ├── __init__.py # Package initialization, version
│ ├── py.typed # PEP 561 marker for type hints
│ ├── cli.py # Command-line interface
│ ├── config.py # Configuration handling
│ ├── constants.py # Constants and enums
│ ├── models/ # Data models
│ │ ├── __init__.py
│ │ └── user.py
│ ├── services/ # Business logic
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── utils/ # Helper functions
│ ├── __init__.py
│ └── helpers.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # pytest fixtures
│ ├── test_models.py
│ └── test_services.py
├── docs/ # Documentation
├── .env.example # Environment template
├── .gitignore
├── pyproject.toml # Project metadata (PEP 621)
└── README.md
pyproject.toml
[project]
name = "mypackage"
version = "0.1.0"
description = "My Python package"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.25.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"ruff>=0.1.0",
"basedpyright>=1.10.0",
]
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
[tool.basedpyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
Development Tooling
UV Commands
| Task |
Command |
| Install package |
uv pip install package-name |
| Install with dev deps |
uv pip install -e ".[dev]" |
| Run script |
uv run python script.py |
| Run tests |
uv run pytest |
| Run type checker |
uv run basedpyright src tests |
Ruff Configuration
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = [
"E",
"F",
"I",
"N",
"W",
"UP",
"B",
"C4",
]
ignore = [
"E501",
]
[tool.ruff.lint.isort]
known-first-party = ["mypackage"]
Pre-commit Hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Quick Reference
Common Commands
| Task |
Command |
| Format code |
ruff format . |
| Lint and fix |
ruff check . --fix |
| Type check |
uv run basedpyright src tests |
| Run tests |
uv run pytest -q |
| Run with coverage |
uv run pytest -q --cov=src --cov-report=term-missing |
Verification Checklist