Skip to main content

Code Style

VoiceGateway enforces consistent code style through automated tooling. This page documents the rules and conventions.

Tooling overview

ToolPurposeConfig location
ruffLinting + formatting (replaces flake8, isort, Black)pyproject.toml [tool.ruff]
mypyStatic type checkingpyproject.toml [tool.mypy]
pre-commitRuns checks before each commit.pre-commit-config.yaml

Ruff

Ruff handles both linting and formatting. It is configured in pyproject.toml:
[tool.ruff]
target-version = "py311"
line-length = 88

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort (import sorting)
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "UP",  # pyupgrade (modernize syntax)
]
ignore = [
    "E501",  # line too long (formatter handles wrapping)
    "B008",  # function calls in argument defaults
    "C901",  # too complex
    "W191",  # indentation contains tabs
]

Running ruff

# Check for lint errors
ruff check .

# Auto-fix what can be fixed
ruff check --fix .

# Format code (Black-compatible)
ruff format .

# Check formatting without modifying files
ruff format --check .

Import sorting

Ruff’s I rule handles import sorting (replacing isort). Imports are grouped in this order:
  1. Standard library (import os, from typing import ...)
  2. Third-party (import pytest, from fastapi import ...)
  3. Local (from voicegateway.core import ...)
Each group is separated by a blank line. Within a group, import statements come before from ... import.

mypy

Static type checking catches bugs before runtime. VoiceGateway’s mypy config:
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
Run mypy:
mypy

Type annotation guidelines

  • All public functions must have type annotations
  • Use from __future__ import annotations at the top of every module (enables PEP 604 X | Y syntax)
  • Use dict, list, tuple (lowercase) instead of Dict, List, Tuple from typing
  • Use X | None instead of Optional[X]
  • Use TYPE_CHECKING guards for import-only types to avoid circular imports:
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from voicegateway.providers.base import BaseProvider

Docstrings

Use Google-style docstrings for all public classes, methods, and functions:
def create_provider(provider_name: str, config: dict[str, Any]) -> BaseProvider:
    """Create a provider instance by name.

    Args:
        provider_name: Name of the provider (e.g., "openai", "deepgram").
        config: Provider configuration dict from voicegw.yaml.

    Returns:
        Initialized provider instance.

    Raises:
        ValueError: If provider name is unknown.
    """
Rules:
  • First line is a concise imperative summary (no period for one-liners)
  • Blank line between summary and Args/Returns/Raises sections
  • Args, Returns, Raises sections as needed
  • Private methods (_foo) may use shorter docstrings

Conventional Commits

All commit messages must follow Conventional Commits:
<type>(<scope>): <description>

[optional body]

[optional footer]

Types

TypeWhen to use
featNew feature
fixBug fix
docsDocumentation only
testAdding or modifying tests
refactorCode change that neither fixes a bug nor adds a feature
perfPerformance improvement
choreBuild process, dependency updates, tooling
ciCI/CD changes

Scopes

Use the module or area affected:
  • core, providers, middleware, storage, pricing
  • dashboard, mcp, cli, server
  • config, docker
  • Provider names: openai, deepgram, etc.

Examples

feat(providers): add ElevenLabs TTS support
fix(middleware): prevent double cost tracking on fallback
docs(mcp): add authentication examples
test(storage): cover edge case in daily aggregation query
refactor(core): extract model resolution from gateway to router
chore(deps): bump livekit-agents to 1.6.0

Multi-scope commits

If a change spans multiple scopes, list the primary scope and mention others in the body:
feat(mcp): implement project tools (list/get/create/delete)

Also updates storage layer to support project deletion
and adds conftest fixtures for MCP testing.

File organization

  • One class per file for providers (openai_provider.py, not providers.py).
  • Group related functions in a module (middleware/cost_tracker.py).
  • Keep __init__.py files minimal — a docstring, re-exports of the subpackage’s public API, and an __all__ declaration. Nothing else.
  • Use from __future__ import annotations in every module.

Internal modules

Files whose names start with a leading underscore are internal implementation details and not part of the public import surface:
  • src/voicegateway/_version.py — hatch-vcs generated, do not edit.
  • src/voicegateway/tests/fixtures/streaming/_loader.py — private test helper.
  • src/voicegateway/inference/_llm.py, _stt.py, _tts.py — private factories; the public surface is voicegateway.inference.{LLM,STT,TTS}.
A future ruff rule could enforce that nothing under src/voicegateway/ imports a leading-underscore module from a different subpackage; for now the convention is documentation-only.

Public API contract

Every package and subpackage __init__.py declares an explicit __all__ list. This is the public surface:
# voicegateway/server/__init__.py
from voicegateway.server.main import build_app

__all__ = ["build_app"]
When __all__ is the empty list (__all__: list[str] = []), the subpackage exposes nothing at its top level and callers reach into submodules directly:
# Use this:
from voicegateway.core.gateway import Gateway

# Not this (would fail because voicegateway.core has __all__ = []):
from voicegateway.core import Gateway
Names not in __all__, and any leading-underscore module, are internal. They may be renamed or removed in any minor release without a deprecation cycle.

Module-level patterns

The codebase converged on a small set of patterns. New code should follow them unless there is a concrete reason not to.

typing.Protocol vs ABC

Prefer typing.Protocol for structural typing where multiple implementations need to satisfy an interface without sharing helper code (see src/voicegateway/cli/tui/data for a real example — the DataClient Protocol is satisfied by both HttpClient and LocalClient without inheritance). Use an abstract base class only when the base genuinely supplies shared behaviour (src/voicegateway/providers/base.py’s BaseProvider is the canonical example: every concrete provider inherits real helper methods).

Pydantic for config

Anything parsed from YAML or environment variables is a Pydantic model. See src/voicegateway/core/config.py and src/voicegateway/core/schema.py for the project-wide config shape; the validators there are the single source of truth for what voicegw.yaml accepts.

Async throughout

Every I/O path uses async / await. Storage reads, provider calls, HTTP handlers, MCP tools, the dashboard backend — all async. Synchronous helpers exist only for pure data transformation (parsing, formatting). When in doubt, make it async; mixing sync and async boundaries is the most common source of subtle bugs in this codebase.

Exception handling

Catch specific exception types where possible. except Exception is acceptable at top-level boundaries (provider call sites, MCP tool dispatch, middleware fallback) where the catch is paired with structured logging and a controlled fallback. Avoid broad excepts in narrow code paths — they hide real bugs and bypass the type system.

Test patterns

See Testing for the full guide. Quick reference:
  • Tests live under src/voicegateway/tests/ mirroring the package layout (src/voicegateway/tests/middleware/ for src/voicegateway/middleware/ tests, etc.).
  • pytest + pytest-asyncio with asyncio_mode = "auto" (configured in pyproject.toml). No @pytest.mark.asyncio is needed; async tests are detected automatically.
  • Shared fixtures live in src/voicegateway/tests/conftest.py. Per-subpackage fixtures live in that subpackage’s conftest.py.
  • File-name pattern: test_<thing-under-test>.py. Function-name pattern: test_<behaviour>.
  • Subprocess CLI tests use the patterns in src/voicegateway/tests/cli/test_record_streaming_fixtures_cli.py.
  • Coverage stays at or above the project gate (see [tool.coverage.run] in pyproject.toml).

Naming conventions

ItemConventionExample
Modulessnake_casecost_tracker.py
ClassesPascalCaseCostTracker
Functionssnake_casecreate_provider
ConstantsUPPER_SNAKE_CASEDEFAULT_DB_PATH
Private_leading_underscore_PROVIDER_REGISTRY
Type varsPascalCase or single letterT