Code Style
Formatting and style conventions. Most of these are enforced by tooling—follow the tools rather than memorizing rules.
Automated Formatting
Backend (Python)
# Format code
ruff format app/
# Lint and auto-fix
ruff check --fix app/
# Type check
mypy app/
Configuration lives in pyproject.toml. Ruff handles:
- Import sorting
- Line length (88 chars)
- Quote style (double)
- Trailing commas
Frontend (TypeScript)
# Format and lint
npm run lint:fix
# Type check
npm run type-check
Configuration lives in .eslintrc and tsconfig.json. ESLint + Prettier handle:
- Import sorting
- Line length (100 chars)
- Quote style (single)
- Semicolons (required)
- Trailing commas (ES5)
Naming Conventions
Python
# Modules: lowercase_with_underscores
my_tool.py
# Classes: PascalCase
class FilterTool:
# Functions/methods: lowercase_with_underscores
async def execute(self):
async def get_output_schema(self):
# Variables: lowercase_with_underscores
input_schema = ...
tool_config = ...
# Constants: UPPERCASE_WITH_UNDERSCORES
DEFAULT_PREVIEW_LIMIT = 100
TypeScript
// Files: PascalCase for components, camelCase for utilities
FilterConfig.tsx
workflowStore.ts
// Components: PascalCase
export function FilterConfig() {}
// Functions: camelCase
function handleChange() {}
// Variables: camelCase
const selectedTool = ...
// Constants: UPPERCASE_WITH_UNDERSCORES
const DEFAULT_LIMIT = 100;
// Types/Interfaces: PascalCase
interface ToolConfig {}
type SocketDirection = 'input' | 'output';
Import Organization
Python
# Standard library
import logging
from pathlib import Path
from typing import Any
# Third-party
import polars as pl
from fastapi import APIRouter
# Local
from app.domain.tools.base import BaseTool
from app.api.models.common import DataSchema
Ruff sorts these automatically.
TypeScript
// React
import { useState, useEffect } from 'react';
// Third-party
import { v4 as uuidv4 } from 'uuid';
// Internal (@/ paths)
import { useWorkflowStore } from '@/state/workflowStore';
import { Tool } from '@/domain/workflow/entities/workflow';
// Relative
import { MyHelper } from './helpers';
Comments
When to Comment
# Explain WHY, not WHAT
# The expression parser doesn't handle nested parentheses correctly,
# so we pre-process to flatten them.
expression = flatten_parens(expression)
# Document non-obvious business logic
# Polars requires at least one column in output; if user deselects all,
# we include the first column to avoid empty schema errors.
if not selected_columns:
selected_columns = [input_schema.columns[0].name]
When Not to Comment
# Bad - states the obvious
# Increment counter
counter += 1
# Bad - should be a better variable name
# Get the user's age
x = user_data["age"]
# Good - self-documenting code
user_age = user_data["age"]
Docstrings
async def execute(
self,
config: dict[str, Any],
inputs: dict[str, pl.LazyFrame]
) -> dict[str, pl.LazyFrame]:
"""
Execute the filter transformation.
Args:
config: Tool configuration with 'expression' key
inputs: Dict mapping socket ID to LazyFrame
Returns:
Dict with 'output-true' and 'output-false' LazyFrames
Raises:
ToolError: If expression is invalid
"""
Use docstrings for public APIs. Internal helpers can skip them if the signature is clear.
File Organization
Python Module
"""Module docstring - one line description."""
# Imports (organized as above)
# Constants
DEFAULT_LIMIT = 100
# Exceptions (if module-specific)
class MyToolError(Exception):
pass
# Main class/function
class MyTool:
...
# Helper functions (private, underscore prefix)
def _parse_expression(expr: str) -> ...:
...
TypeScript Component
// Imports
// Types (if component-specific)
interface Props {
tool: Tool;
}
// Constants
const DEFAULT_VALUE = 0;
// Component
export function MyComponent({ tool }: Props) {
// Hooks
// Handlers
// Render
}
// Helpers (if component-specific)
function formatValue(value: number): string {
return value.toFixed(2);
}
Error Messages
Write error messages for the user, not the developer:
# Bad
raise ToolError("NoneType has no attribute 'columns'")
# Good
raise ToolError("No input connected. Connect a data source to the input socket.")
# Bad
raise ValidationError("Invalid regex")
# Good
raise ValidationError(
f"Invalid filter expression: '{expression}'. "
"Check for unmatched parentheses or invalid column names."
)
Line Length
- Python: 88 characters (Black default)
- TypeScript: 100 characters
Break long lines logically:
# Good
result = some_function(
first_argument,
second_argument,
third_argument,
)
# Good
long_condition = (
first_thing
and second_thing
and third_thing
)
// Good
const result = someFunction(
firstArgument,
secondArgument,
thirdArgument
);
// Good
const isValid =
hasRequiredField &&
passesValidation &&
meetsCondition;
Commit Messages
# Format
<type>: <description>
# Types
feat: Add new feature
fix: Bug fix
docs: Documentation only
refactor: Code change that neither fixes nor adds
test: Adding or updating tests
chore: Maintenance tasks
# Examples
feat: Add Pivot tool implementation
fix: Handle empty input in Filter tool
docs: Add backend patterns guide
refactor: Extract schema logic to helper
test: Add edge cases for Formula tool
Keep the first line under 72 characters. Add a blank line and more detail if needed.
Pre-Commit Checks
Before committing, run:
# Backend
cd backend
ruff format app/
ruff check --fix app/
mypy app/
pytest
# Frontend
cd frontend
npm run lint:fix
npm run type-check
npm test
Or use the script:
./scripts/test.sh
If this passes, your code meets style requirements.
Next: Testing Philosophy.