Skip to content

Latest commit

 

History

History
305 lines (232 loc) · 9.74 KB

File metadata and controls

305 lines (232 loc) · 9.74 KB

Python Coding Standards

This document outlines the coding standards and best practices for Python development across all Bayat projects. Following these guidelines ensures code consistency, quality, and maintainability.

Table of Contents

Python Version

  • Use Python 3.11+ for all new projects. This provides access to significant performance improvements, better error messages, and modern features like the tomllib module and typing.Self.
  • Document the specific Python version in pyproject.toml.

Code Style and Linting

Formatting and Linting with Ruff

We standardize on Ruff for linting, formatting, and import sorting. Ruff is an extremely fast, all-in-one tool that replaces the need for separate tools like Black, isort, and flake8.

  • Configuration: All configuration should be in the [tool.ruff] section of your pyproject.toml.
  • Line Length: Maximum line length is 88 characters.
  • Rules: Start with a conservative but sensible set of rules.

Example pyproject.toml configuration for Ruff:

[tool.ruff]
line-length = 88
select = [
    "E",  # pycodestyle errors
    "W",  # pycodestyle warnings
    "F",  # pyflakes
    "I",  # isort
    "C4", # flake8-comprehensions
    "B",  # flake8-bugbear
]
ignore = ["B008"] # Do not complain about function calls in default arguments

[tool.ruff.lint]
# Allow unused variables in `__init__.py` files
exclude = ["__init__.py"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

Naming Conventions

  • Packages: lowercase, short, no underscores (e.g., utils, models)
  • Modules: lowercase_with_underscores (e.g., data_processor.py)
  • Classes: PascalCase (e.g., DataProcessor)
  • Functions/Methods: snake_case (e.g., process_data())
  • Variables: snake_case (e.g., user_input)
  • Constants: UPPER_SNAKE_CASE (e.g., MAX_CONNECTIONS)
  • Private attributes/methods: Prefixed with a single underscore (e.g., _internal_method())

Project Structure

A standardized project structure is crucial. We recommend the src layout.

Library/Package Structure

my_package/
├── pyproject.toml      # Project metadata, dependencies, and tool config
├── README.md
├── LICENSE
├── .gitignore
└── src/
    └── my_package/
        ├── __init__.py
        └── ...
└── tests/
    └── test_...

Web Application Structure (e.g., FastAPI)

my_webapp/
├── pyproject.toml
├── README.md
├── .env.example
├── .gitignore
└── src/
    └── my_webapp/
        ├── __init__.py
        ├── main.py         # Application entry point
        ├── api/            # API endpoints/routers
        ├── core/           # Core logic, config, etc.
        ├── models/         # Data models (e.g., Pydantic models)
        └── services/       # Business logic
└── tests/
    └── ...

Dependency Management

  • Poetry is the standard tool for dependency management and packaging in all new Python projects. It provides robust dependency resolution, environment management, and build capabilities.
  • Define dependencies in pyproject.toml.
  • Use poetry lock to create a poetry.lock file, which ensures deterministic builds. Always commit the poetry.lock file.
  • Use dependency groups (e.g., [tool.poetry.group.dev.dependencies]) to separate development dependencies from production ones.

Example pyproject.toml with Poetry:

[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "My awesome Python project"
authors = ["Your Name <your.email@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.95.0"
uvicorn = "^0.21.0"
pydantic = {extras = ["email"], version = "^1.10.7"}

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
ruff = "^0.0.250"

Type Hinting

Modern, explicit type hints are required for all new code.

  • Use type hints for all function parameters and return values.
  • Use the | syntax for union types (e.g., int | str) instead of Union[int, str].
  • Use built-in generic types (list, dict) instead of typing.List and typing.Dict.
  • Use typing.Literal for variables that can only take on specific literal values.

Example of modern type hinting:

from typing import Literal, Annotated
from pydantic import BaseModel, Field

# Type alias
UserRole = Literal["admin", "editor", "viewer"]

class User(BaseModel):
    username: str
    role: UserRole
    age: Annotated[int, Field(gt=0, description="User's age, must be positive")]

def get_user_by_role(role: UserRole) -> list[User]:
    """Retrieves users with a specific role."""
    # ... implementation
    return []

Asynchronous Programming

For I/O-bound applications (like web servers), asyncio should be the default choice.

  • Use async and await syntax for all I/O operations (database calls, API requests, file system access).
  • FastAPI is the recommended framework for building new asynchronous web APIs.
  • Use libraries that support asyncio, such as httpx for HTTP requests and asyncpg for PostgreSQL.

Example of an async function:

import asyncio
import httpx

async def fetch_data_from_url(url: str) -> dict | None:
    """Asynchronously fetches JSON data from a URL."""
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, timeout=10.0)
            response.raise_for_status()  # Raise an exception for 4xx/5xx responses
            return response.json()
        except httpx.HTTPStatusError as e:
            print(f"HTTP error occurred: {e}")
            return None
        except httpx.RequestError as e:
            print(f"An error occurred while requesting {e.request.url!r}.")
            return None

async def main():
    data = await fetch_data_from_url("https://api.example.com/data")
    if data:
        print("Successfully fetched data.")

if __name__ == "__main__":
    asyncio.run(main())

Documentation

Docstrings

Use Google-style docstrings for all public modules, classes, and functions.

def calculate_summary_statistics(data: list[float]) -> dict[str, float]:
    """Calculate summary statistics for a list of numeric values.

    Args:
        data: A list of numeric values to analyze.

    Returns:
        A dictionary containing the summary statistics.

    Raises:
        ValueError: If the input data is empty.
    """
    # ... implementation

Project Documentation

  • Maintain a comprehensive README.md with setup, configuration, and usage instructions.
  • For larger projects, use MkDocs or Sphinx to generate a full documentation website.

Testing

  • Use pytest as the standard testing framework.
  • Organize tests in a top-level tests/ directory that mirrors the src/ directory structure.
  • For detailed testing guidelines, refer to the Testing Standards.

Error Handling

  • Use specific, built-in exceptions whenever possible.
  • Create a custom exception hierarchy for application-specific errors.
  • Avoid catching generic Exception unless you are re-raising it.
  • Log exceptions with context rather than printing to standard output.

Security Best Practices

  • Never store secrets in code. Use environment variables and a secrets management tool.
  • Sanitize all user input to prevent injection attacks.
  • Use ruff's security rules (B category) to catch common vulnerabilities.
  • For detailed security guidelines, refer to the Secure Coding Standards.

Packaging and Deployment

  • Use poetry build to create standard wheel and sdist packages.
  • For deploying applications, use Docker. See the Containerization Standards for detailed guidelines.

Example Multi-Stage Dockerfile:

# --- Builder Stage ---
FROM python:3.11-slim as builder

WORKDIR /app

# Install poetry
RUN pip install poetry

# Copy only files needed for dependency installation
COPY poetry.lock pyproject.toml ./

# Install dependencies
RUN poetry install --no-root --no-dev

# --- Final Stage ---
FROM python:3.11-slim

WORKDIR /app

# Create a non-root user
RUN adduser --disabled-password --gecos "" appuser

# Copy installed dependencies from builder stage
COPY --from=builder /app/.venv ./.venv

# Copy application source code
COPY src/ ./src

# Set PATH to include the virtual environment
ENV PATH="/app/.venv/bin:$PATH"

USER appuser

CMD ["uvicorn", "src.my_webapp.main:app", "--host", "0.0.0.0", "--port", "8000"]

Environment Management

  • Use poetry's environment management (poetry shell, poetry run) for local development.
  • Use .env files for managing environment variables locally. Never commit .env files.
  • Provide a .env.example file with placeholder values for all required environment variables.
  • Use a library like pydantic to load and validate environment variables into a typed settings object.