Code Style Guide

This guide covers coding standards and style conventions for Request Forge.

Code Formatting

We use Ruff for code formatting and linting.

Ruff Configuration

# .ruff.toml
target-version = "py310"
fix = true
line-length = 79
indent-width = 4

[lint]
select = [
    "E",        # pycodestyle errors
    "W",        # pycodestyle warnings
    "F",        # pyflakes
    "I",        # isort
    "B",        # flake8-bugbear
    "C4",       # flake8-comprehensions list/set/dict
    "UP",       # pyupgrade
    "ARG",      # flake8-unused-arguments
    "SIM",      # flake8-simplify
    'S',        # security
    'F',        # flake8
    'PIE',      # improving code quality and readability
    'A',        # builtin names used
    'E',        # errors
    'W',        # warnings
    'TID',      # tidier imports
    'TCH',      # imports for type checking
    'N',        # naming
    'D419',     # doc style
    'DJ',       # django
    'ICN',      # import conventions
    'ASYNC',    # async
    'T20',      # found print
    'PT',       # pytest style
    'RSE',      # Unnecessary parentheses on raised exception
    'RET',      # checks returns
    'TD',       # todos comments
    'FIX',      # fix me comments
]
extend-ignore = [
    'A003',
    'UP031',    # Use format specifiers instead of percent format
    'S101',     # Use of `assert` detected
    'W293',     # Blank line contains whitespace
    'S105',     # Possible hardcoded password assigned
    'S106',     # Possible hardcoded password assigned
    'S107',     # Possible hardcoded password assigned
    'F841',     # Remove assignment to unused variable
    'N818',     # Check exception names
    'ARG002',   # Unused method argument
    'ARG005',   # Unused lambda argument
    'SIM117',   # Use a single with statement
]
ignore = [
    "E501",  # line too long (handled by formatter)
    "B008",  # do not perform function calls in argument defaults
    "B904",  # raise from
]

[lint.per-file-ignores]
"__init__.py" = ["F401", "F403"]
"tests/*" = ["B011", "ARG001"]

[format]
quote-style = "single"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[lint.isort]
known-first-party = ["requestforge"]
from-first = true
length-sort = true
order-by-type = true
case-sensitive = true
force-wrap-aliases = true
combine-as-imports = true
detect-same-package = true
length-sort-straight = true
split-on-trailing-comma = true
force-sort-within-sections = true
lines-after-imports = 2
no-lines-before = ['future', ]
known-third-party = ['pytest']
section-order = ['future', 'standard-library', 'third-party', 'local-folder']

Running Ruff

# Check code
ruff check src/ tests/

# Fix auto-fixable issues
ruff check --fix src/ tests/

# Format code
ruff format src/ tests/

# Check formatting
ruff format --check src/ tests/

Python Style

PEP 8 Compliance

Follow PEP 8 with these exceptions:

  • Line length: 120 characters (instead of 79)

  • String quotes: Single quotes preferred ('text' instead of "text")

Imports

Order imports using isort style:

# Standard library
import os
import sys
from typing import Any

# Third-party
import requests
from requests.adapters import HTTPAdapter

# Local
from requestforge.config import HttpClientConfig
from requestforge.exceptions import HttpClientException

Rules:

  • Group imports: stdlib, third-party, local

  • Sort alphabetically within groups

  • Use absolute imports

  • Avoid wildcard imports (from module import *)

Type Hints

Use type hints for all public APIs:

# Good ✅
def get(
    self,
    url: str,
    params: dict[str, Any] | None = None,
    headers: dict[str, str] | None = None,
    timeout: float | None = None,
) -> HttpResponse:
    """Execute a GET request."""
    ...

# Avoid ❌
def get(self, url, params=None, headers=None):
    ...

Type Hint Guidelines:

  • Use built-in generic types (list[str], not List[str])

  • Use | for unions (str | None, not Optional[str])

  • Use from __future__ import annotations for forward references

  • Use TYPE_CHECKING for import-only types

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterator

Naming Conventions

Classes

  • Use PascalCase

  • Descriptive names

  • Interfaces end with Interface

# Good ✅
class HttpClient:
    ...

class RetryStrategyInterface:
    ...

class ExponentialBackoffRetryStrategy:
    ...

Functions and Methods

  • Use snake_case

  • Descriptive verb-based names

  • Private methods start with _

# Good ✅
def get_token(self):
    ...

def _build_request_headers(self, context):
    ...

# Avoid ❌
def getTok(self):
    ...

Variables

  • Use snake_case

  • Descriptive names

  • Private variables start with _

  • Constants use UPPER_CASE

# Good ✅
user_token = 'abc123'
_cache_key = 'tokens:user'
MAX_RETRIES = 3

# Avoid ❌
ut = 'abc123'
maxRetries = 3

Documentation

Docstrings

Use Google-style docstrings:

def fetch_token(self, context: dict | None = None) -> TokenData:
    """
    Fetch token for this step.

    Args:
        context: Dictionary mapping step names to TokenData from
                 previous steps. None if this is the first step.

    Returns:
        TokenData containing the fetched token.

    Raises:
        AuthenticationException: If token fetch fails.

    Example:
        >>> fetcher = BodyTokenFetcher(...)
        >>> token = fetcher.fetch()
        >>> print(token.access_token)
    """
    ...

Docstring Sections:

  • Short summary (one line)

  • Detailed description (optional)

  • Args: Parameter descriptions

  • Returns: Return value description

  • Raises: Exceptions that can be raised

  • Example: Usage examples (optional)

  • Note: Additional notes (optional)

Module Docstrings

"""
Retry strategy implementations with different patterns.

This module provides various retry strategies including:
- NoRetryStrategy: No retries
- ExponentialBackoffRetryStrategy: Exponential backoff with jitter
- CircuitBreakerRetryStrategy: Circuit breaker pattern
"""

Comments

# Good ✅ - Explain why, not what
# Jitter prevents thundering herd when many clients retry simultaneously
delay += random.uniform(-jitter_range, jitter_range)

# Avoid ❌ - Obvious comments
# Add jitter to delay
delay += random.uniform(-jitter_range, jitter_range)

Code Organization

Class Structure

Organize class members in this order:

  1. Class variables

  2. __init__

  3. Properties

  4. Public methods

  5. Private methods

  6. Magic methods (__str__, __repr__, etc.)

class HttpClient:
    # Class variables
    DEFAULT_TIMEOUT = 30.0

    # Initialization
    def __init__(self, config):
        self._config = config

    # Properties
    @property
    def session(self):
        return self._session

    # Public methods
    def get(self, url):
        ...

    def post(self, url, data):
        ...

    # Private methods
    def _build_url(self, url):
        ...

    def _execute_request(self, request):
        ...

    # Magic methods
    def __enter__(self):
        ...

    def __exit__(self, exc_type, exc_val, exc_tb):
        ...

Function Length

  • Keep functions focused and short

  • Aim for < 50 lines

  • Extract complex logic to separate functions

# Good ✅ - Focused function
def should_retry(self, context, exception):
    if context.attempt >= self._max_retries:
        return False
    return self._is_retryable_exception(exception)

def _is_retryable_exception(self, exception):
    return isinstance(exception, (TimeoutException, ConnectionException))

# Avoid ❌ - Too long, doing too much
def should_retry(self, context, exception):
    # 100 lines of logic...

Best Practices

Immutability

Prefer immutable objects:

# Good ✅ - Immutable dataclass
@dataclass(frozen=True)
class HttpRequest:
    method: HttpMethod
    url: str

    def with_headers(self, headers: dict) -> HttpRequest:
        """Return new instance with headers."""
        return HttpRequest(
            method=self.method,
            url=self.url,
            headers={**self.headers, **headers}
        )

SOLID Principles

Follow SOLID principles:

# Good ✅ - Interface segregation
class RequestHookInterface:
    def before_request(self, request, context):
        ...

class ResponseHookInterface:
    def after_response(self, response, context):
        ...

# Avoid ❌ - Fat interface
class HookInterface:
    def before_request(self, request, context):
        ...
    def after_response(self, response, context):
        ...
    def on_error(self, exception, context):
        ...

Error Handling

# Good ✅ - Specific exceptions
if token.is_expired:
    raise AuthenticationException('Token expired')

# Avoid ❌ - Generic exceptions
if token.is_expired:
    raise Exception('Error')

Rules:

  • Use specific exception types

  • Include meaningful error messages

  • Provide context in exceptions

  • Don’t catch exceptions you can’t handle

Resource Management

# Good ✅ - Context manager
with HttpClient('https://api.example.com') as client:
    response = client.get('/users')

# Good ✅ - Explicit cleanup
client = HttpClient(config)
try:
    response = client.get('/users')
finally:
    client.close()

Constants

# Good ✅ - Named constants
DEFAULT_TIMEOUT = 30.0
MAX_RETRIES = 3
RETRY_STATUS_CODES = frozenset({408, 429, 500, 502, 503, 504})

if status_code in RETRY_STATUS_CODES:
    ...

# Avoid ❌ - Magic numbers
if status_code in {408, 429, 500, 502, 503, 504}:
    ...

Example Code

Well-Styled Module

"""
Request Forge retry strategies.

This module provides retry strategy implementations following
the Strategy pattern.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
import secrets

from requestforge.exceptions import TimeoutException, ConnectionException

if TYPE_CHECKING:
    from requestforge.models import RequestContext

# Constants
DEFAULT_MAX_RETRIES = 3
DEFAULT_BASE_DELAY = 1.0
DEFAULT_MAX_DELAY = 60.0


class RetryStrategyInterface(ABC):
    """Abstract interface for retry strategies."""

    @property
    @abstractmethod
    def max_retries(self) -> int:
        """Maximum number of retry attempts."""

    @abstractmethod
    def should_retry(self, context: RequestContext, exception: Exception) -> bool:
        """
        Determine if request should be retried.

        Args:
            context: Request context with attempt count
            exception: Exception that caused failure

        Returns:
            True if should retry, False otherwise
        """

    @abstractmethod
    def get_delay(self, context: RequestContext) -> float:
        """
        Get delay in seconds before next retry.

        Args:
            context: Request context

        Returns:
            Delay in seconds
        """


class ExponentialBackoffRetryStrategy(RetryStrategyInterface):
    """
    Exponential backoff retry strategy with jitter.

    Delay formula: min(base_delay * (multiplier ^ attempt) + jitter, max_delay)

    Example:
        >>> strategy = ExponentialBackoffRetryStrategy(
        ...     max_retries=3,
        ...     base_delay=1.0,
        ...     max_delay=60.0,
        ...     multiplier=2.0,
        ...     jitter=True
        ... )
    """

    def __init__(
        self,
        max_retries: int = DEFAULT_MAX_RETRIES,
        base_delay: float = DEFAULT_BASE_DELAY,
        max_delay: float = DEFAULT_MAX_DELAY,
        multiplier: float = 2.0,
        jitter: bool = True,
    ):
        self._max_retries = max_retries
        self._base_delay = base_delay
        self._max_delay = max_delay
        self._multiplier = multiplier
        self._jitter = jitter

    @property
    def max_retries(self) -> int:
        return self._max_retries

    def should_retry(self, context: RequestContext, exception: Exception) -> bool:
        if context.attempt >= self._max_retries:
            return False

        return isinstance(exception, (TimeoutException, ConnectionException))

    def get_delay(self, context: RequestContext) -> float:
        # Calculate exponential delay
        delay = self._base_delay * (self._multiplier ** context.attempt)

        # Add jitter if enabled
        if self._jitter:
            jitter_range = delay * 0.25
            delay += (
                secrets.randbelow(int(jitter_range * 2 * 1_000_000)) / 1_000_000
                - jitter_range
            )

        # Cap at max_delay
        return min(max(delay, 0), self._max_delay)

Pre-commit Configuration

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.1.9
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: check-merge-conflict

See Also