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.
- Python Version
- Code Style and Linting
- Project Structure
- Dependency Management
- Type Hinting
- Asynchronous Programming
- Documentation
- Testing
- Error Handling
- Security Best Practices
- Packaging and Deployment
- Environment Management
- Use Python 3.11+ for all new projects. This provides access to significant performance improvements, better error messages, and modern features like the
tomllibmodule andtyping.Self. - Document the specific Python version in
pyproject.toml.
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 yourpyproject.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"- 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())
A standardized project structure is crucial. We recommend the src layout.
my_package/
├── pyproject.toml # Project metadata, dependencies, and tool config
├── README.md
├── LICENSE
├── .gitignore
└── src/
└── my_package/
├── __init__.py
└── ...
└── tests/
└── test_...
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/
└── ...
- 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 lockto create apoetry.lockfile, which ensures deterministic builds. Always commit thepoetry.lockfile. - 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"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 ofUnion[int, str]. - Use built-in generic types (
list,dict) instead oftyping.Listandtyping.Dict. - Use
typing.Literalfor 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 []For I/O-bound applications (like web servers), asyncio should be the default choice.
- Use
asyncandawaitsyntax 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 ashttpxfor HTTP requests andasyncpgfor 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())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- Maintain a comprehensive
README.mdwith setup, configuration, and usage instructions. - For larger projects, use MkDocs or Sphinx to generate a full documentation website.
- Use pytest as the standard testing framework.
- Organize tests in a top-level
tests/directory that mirrors thesrc/directory structure. - For detailed testing guidelines, refer to the Testing Standards.
- Use specific, built-in exceptions whenever possible.
- Create a custom exception hierarchy for application-specific errors.
- Avoid catching generic
Exceptionunless you are re-raising it. - Log exceptions with context rather than printing to standard output.
- 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 (Bcategory) to catch common vulnerabilities. - For detailed security guidelines, refer to the Secure Coding Standards.
- Use
poetry buildto 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"]- Use
poetry's environment management (poetry shell,poetry run) for local development. - Use
.envfiles for managing environment variables locally. Never commit.envfiles. - Provide a
.env.examplefile with placeholder values for all required environment variables. - Use a library like
pydanticto load and validate environment variables into a typed settings object.