Skip to content

Code Style Guide

Victor enforces consistent code style through automated tools. This guide covers formatting, linting, and style conventions.

Quick Reference

# Format code (run before commits)
make format

# Check code style (CI runs this)
make lint

# Individual tools
black victor tests                    # Format with Black
ruff check --fix victor tests         # Lint and auto-fix with Ruff
mypy                                  # Configured core + SDK type check

Tools Overview

Tool Purpose Configuration
Black Code formatting pyproject.toml [tool.black]
Ruff Linting (replaces flake8) pyproject.toml [tool.ruff]
MyPy Type checking pyproject.toml [tool.mypy]

Black Formatting

Black is our code formatter with zero configuration decisions. It ensures consistent style across the codebase.

Configuration

From pyproject.toml:

[tool.black]
line-length = 100
target-version = ['py312']
include = '\.pyi?$'

Key Rules

  • Line length: 100 characters (not the default 88)
  • Target version: Python 3.12+
  • String quotes: Double quotes preferred
  • Trailing commas: Added in multi-line structures

Running Black

# Format all code
black victor tests

# Check without modifying (CI mode)
black --check victor tests

# Show what would change
black --diff victor tests

Examples

# Before Black
def function(arg1,arg2,arg3):return arg1+arg2+arg3

# After Black
def function(arg1, arg2, arg3):
    return arg1 + arg2 + arg3
# Long function calls are split with trailing commas
result = some_function(
    argument_one="value",
    argument_two="another_value",
    argument_three="third_value",  # Trailing comma
)

Ruff Linting

Ruff is a fast Python linter that replaces flake8, isort, and other tools.

Configuration

From pyproject.toml:

[tool.ruff]
line-length = 100
target-version = "py312"
extend-exclude = [
    "archive",
    "examples",
    "venv",
]

[tool.ruff.lint]
select = [
    "E",  # pycodestyle errors
    "W",  # pycodestyle warnings
    "F",  # pyflakes
    "B",  # flake8-bugbear
    "C4", # flake8-comprehensions
]
ignore = [
    "E501",  # line too long (handled by Black)
    "E402",  # module level import not at top
    "F401",  # unused imports (often intentional re-exports)
    "F811",  # redefined while unused
    "B007",  # loop control variable not used
    "B008",  # function calls in argument defaults
]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]  # Allow unused imports in __init__.py
"tests/**/*.py" = ["B017", "B018", "E741", "F841"]

Running Ruff

# Check for issues
ruff check victor tests

# Auto-fix issues
ruff check --fix victor tests

# Show all issues including fixable
ruff check --show-fixes victor tests

Common Ruff Rules

Rule Description Example
E pycodestyle errors Missing whitespace, indentation
W pycodestyle warnings Trailing whitespace
F pyflakes Undefined names, unused imports
B bugbear Common bugs and design issues
C4 comprehensions Unnecessary list/dict calls

Fixing Common Issues

# F401: Unused import - remove or use
import os  # Remove if unused

# F841: Unused variable - prefix with underscore
_result = function()  # Explicitly unused

# B006: Mutable default argument
def bad(items=[]):  # Don't do this
    pass

def good(items=None):  # Do this instead
    items = items or []

MyPy Type Checking

MyPy verifies type hints for type safety and better IDE support.

Configuration and qualification

pyproject.toml is the authoritative configuration. MyPy is pinned to 2.4.0 across
core, SDK and vertical developer environments; CI reads the root CI pin through
scripts/ci/release_contract.py mypy. This stable release supports Python >=3.10;
Victor checks against its supported Python 3.12 floor. Update the pin and qualify
the diagnostic changes together. Strict flags are enabled, but
legacy victor.* error suppression remains except for explicitly qualified
modules. A successful configured check is therefore not whole-repository typing
acceptance. The SDK and its legacy root are included without that suppression.
G96
tracks the unsuppressed inventory and ordered removal of exceptions, including
independent packages and tests/scripts.

Running MyPy

# Configured source targets: core, contracts SDK and compatibility root
.venv-codesign/bin/python -m mypy

# Same strict invocation as CI; legacy exceptions still apply
.venv-codesign/bin/python -m mypy --strict

# Inspect one module; its configured exception, if any, still applies
.venv-codesign/bin/python -m mypy victor/agent/orchestrator.py --show-error-codes

New code must not copy broad suppressions. Fix types at the owning contract and
retain runtime tests; annotations alone do not validate external data or prove
safe execution. Built SDK wheels also undergo downstream positive/negative checks.

Type Hint Requirements

Type hints are required on all public APIs:

from typing import List, Dict, Optional, Any, AsyncIterator

def public_function(
    name: str,
    count: int,
    options: Optional[Dict[str, Any]] = None,
) -> List[str]:
    """Public functions need type hints."""
    pass

async def async_function(
    items: List[str],
) -> AsyncIterator[str]:
    """Async functions too."""
    for item in items:
        yield item

Common Type Patterns

from typing import TYPE_CHECKING, Union, Callable, TypeVar

# Type-only imports (no runtime cost)
if TYPE_CHECKING:
    from victor.providers.base import BaseProvider

# Union types
def process(value: Union[str, int]) -> str:
    return str(value)

# Generic types
T = TypeVar("T")
def first(items: List[T]) -> Optional[T]:
    return items[0] if items else None

# Callable types
Handler = Callable[[str], None]
def register_handler(handler: Handler) -> None:
    pass

Docstrings

Victor uses Google-style docstrings for documentation.

Function Docstrings

def complex_function(
    arg1: str,
    arg2: int,
    optional_arg: Optional[List[str]] = None,
) -> Dict[str, Any]:
    """Short one-line summary.

    Longer description explaining what the function does,
    why it exists, and any important details. This can span
    multiple lines.

    Args:
        arg1: Description of arg1. If the description is long,
            it can continue on the next line with indentation.
        arg2: Description of arg2.
        optional_arg: Description of optional argument.
            Defaults to None.

    Returns:
        Description of return value. For complex return types,
        describe the structure:
        - key1: Description of key1
        - key2: Description of key2

    Raises:
        ValueError: When arg1 is empty.
        TypeError: When arg2 is not an integer.

    Examples:
        >>> result = complex_function("test", 42)
        >>> result["status"]
        'success'
    """
    pass

Class Docstrings

class MyClass:
    """Short summary of the class.

    Longer description of the class, its purpose, and usage.
    Include any important notes about lifecycle or state.

    Attributes:
        name: The name of the instance.
        config: Configuration dictionary.
        _internal: Private attribute (not documented usually).

    Examples:
        >>> obj = MyClass("example")
        >>> obj.name
        'example'
    """

    def __init__(self, name: str, config: Optional[Dict] = None) -> None:
        """Initialize MyClass.

        Args:
            name: The name for this instance.
            config: Optional configuration dictionary.
        """
        self.name = name
        self.config = config or {}

Module Docstrings

"""Module providing XYZ functionality.

This module contains classes and functions for handling XYZ.
It is part of the larger ABC system.

Typical usage:
    from victor.module import MyClass

    instance = MyClass()
    result = instance.process()

See Also:
    victor.related_module: Related functionality
    victor.other_module: Other related module
"""

Import Organization

Imports should be organized in groups:

# Standard library imports
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional

# Third-party imports
import httpx
from pydantic import BaseModel

# Local imports
from victor.config import Settings
from victor.tools.base import BaseTool
from victor.providers.base import BaseProvider

Ruff handles import sorting automatically with --fix.

Naming Conventions

Type Convention Example
Modules lowercase_with_underscores tool_registry.py
Classes PascalCase AgentOrchestrator
Functions lowercase_with_underscores execute_tool()
Constants UPPERCASE_WITH_UNDERSCORES MAX_RETRIES
Private Leading underscore _internal_method()
Type Variables Single uppercase or PascalCase T, KeyType

Examples

# Constants
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30.0

# Classes
class MyToolExecutor:
    """Tool executor class."""

    def __init__(self) -> None:
        self._internal_state = {}  # Private attribute

    def execute_tool(self, tool_name: str) -> None:
        """Public method."""
        self._validate_tool(tool_name)

    def _validate_tool(self, name: str) -> None:
        """Private method."""
        pass

Package & Directory Naming (Singular vs Plural)

Package and directory names follow one rule so that the same concept never
appears as both foo/ and foos/ siblings:

  1. Top-level packages and their tests/ mirrors are plural when they hold
    a collection of modules: victor/tools, victor/providers, victor/protocols,
    victor/verticals, mirrored by tests/unit/tools, tests/unit/providers, …
    A test directory mirrors its source package name exactly.
  2. Singular is for a sub-package describing one concrete subsystem or a
    proper/mass noun: the victor/benchmark vertical, mode_config, profiler.
  3. A category test directory that is not a package mirror uses a descriptive
    name, never the bare noun — tests/performance, not tests/benchmark.
  4. No parent holds both forms of one concept. If a stem names two distinct
    things, disambiguate by renaming one (e.g. agents/ meaning multi-agent
    coordination → multi_agent/).
# Good — plural collection, test mirror matches source
victor/tools/              tests/unit/tools/

# Good — singular concrete subsystem (proper noun)
victor/benchmark/          tests/unit/benchmark/

# Bad — same concept as both singular and plural siblings (now folded)
tests/benchmark/  +  tests/benchmarks/        →  tests/benchmarks/
victor/integrations/protocol/  +  .../protocols/  →  query_enhancement moved to victor/protocols/

Cross-layer splits where singular and plural describe genuinely different things
at different layers are fine and documented, not folded — e.g.
victor/agent/conversation (the conversation runtime/store) vs
victor/framework/conversations (the coordination framework), and the
benchmark vertical vs victor/evaluation/benchmarks (eval-dataset suites).

This convention is enforced in CI by
tests/unit/runtime/test_naming_singular_plural_guard.py, which fails on any
new same-parent singular/plural directory collision. That guard test is the
authoritative statement of the rule; update it (and its allowlist) if the
convention changes.

Async/Await Conventions

All I/O operations should be async:

# Good: Async I/O
async def fetch_data(url: str) -> Dict[str, Any]:
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.json()

# Good: Async file operations
async def read_file(path: Path) -> str:
    import aiofiles
    async with aiofiles.open(path) as f:
        return await f.read()

# Bad: Blocking I/O in async context
async def bad_fetch(url: str) -> Dict[str, Any]:
    import requests  # Don't use sync requests
    response = requests.get(url)  # This blocks!
    return response.json()

Line Length

Maximum line length is 100 characters. Black handles wrapping automatically.

# Good: Within 100 characters
result = process_data(input_value, config=settings)

# Good: Black wraps long lines
result = some_very_long_function_name(
    first_argument="value",
    second_argument="another_value",
    third_argument="yet_another_value",
)

# Good: String continuation
message = (
    "This is a very long message that needs to be split "
    "across multiple lines for readability."
)

Pre-commit Integration

Victor supports pre-commit hooks. Create .pre-commit-config.yaml:

repos:
  - repo: https://github.com/psf/black
    rev: 24.4.0
    hooks:
      - id: black
        args: [--line-length=100]

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix]

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        args: [--ignore-missing-imports]
        additional_dependencies: [types-pyyaml, types-aiofiles]

Install and run:

pip install pre-commit
pre-commit install
pre-commit run --all-files

CI/CD Checks

All pull requests must pass these checks:

# These commands run in CI
ruff check victor             # No lint errors
black --check victor          # Code is formatted
mypy                                # Configured gate; G96 tracks unsuppressed debt

If CI fails:

# Fix formatting
make format

# Check what's wrong
make lint

Common Style Issues

Issue: Long Import Lines

# Bad
from victor.framework import Agent, EventType, StateGraph, ToolSet

# Good
from victor.framework import (
    Agent,
    EventType,
    StateGraph,
    ToolSet,
)

Issue: Missing Type Hints on Public API

# Bad
def get_tools(category):
    pass

# Good
def get_tools(category: str) -> List[BaseTool]:
    pass

Issue: Mutable Default Arguments

# Bad
def process(items=[]):
    items.append("new")
    return items

# Good
def process(items: Optional[List[str]] = None) -> List[str]:
    items = items or []
    items.append("new")
    return items

Issue: Bare Except Clauses

# Bad
try:
    risky_operation()
except:
    pass

# Good
try:
    risky_operation()
except Exception as e:
    logger.error(f"Operation failed: {e}")

Next Steps


Questions? Open an issue on GitHub or start a discussion.