Development Style Guide & Project Rules
| Status | Applies to | Owner |
|---|---|---|
| Pre-release draft | main branch as of 2026-06-30 coding and review rules | Development maintainers |
1. Core Philosophy
Optimize for explicit, self-documenting code over terse keystroke-saving. Prioritize legibility and mathematical clarity. Engineers should never have to guess what variables represent.
2. Python Guidelines
Tooling & Environment
- Package Management: Strictly managed by uv.
- Linting & Formatting: Managed entirely by ruff (ruff format is the source of truth).
Imports
- Rule: Default to module-qualified imports. Import the module, then access members through its namespace.
- Good:
import scipy.stats;scipy.stats.norm(...) - Good:
import sklearn.linear_model;sklearn.linear_model.LinearRegression(...) - Bad:
from scipy.stats import norm - Bad:
from sklearn.linear_model import LinearRegression - Rule: The only approved direct-import exceptions are
from pathlib import Pathandfrom dataclasses import dataclass. - Rule: Import
typingas a module and qualify all names. Never import fromtyping. - Good:
import typing;typing.TYPE_CHECKING;typing.Any - Bad:
from typing import TYPE_CHECKING, Any - Rule: Import
enumandcollections.abcas modules and qualify all names. - Good:
import enum;enum.StrEnum - Good:
import collections.abc;collections.abc.Iterator - Bad:
from enum import StrEnum - Bad:
from collections.abc import Iterator - Rule: Use conventional aliases only where they are already standard and improve readability.
- Approved runtime examples:
import numpy as np,import numpy.typing as npt,import jax.numpy as jnp - Polars may be used in dev scripts or tests only, not runtime
src/gcode. - Rule: Keep imports out of functions, methods, and classes in production code under
src/gunless the local import is an explicit runtime boundary. - Module-scope imports are the default.
if typing.TYPE_CHECKING:blocks are allowed for annotation-only imports.- Allowed local imports:
- runtime boundary imports that intentionally delay JAX or native module initialization
- optional dependency helpers
typing.TYPE_CHECKINGannotation-only imports
- Not allowed: casual local imports to avoid circular dependencies.
- Tests may use local imports when there is a concrete reason, such as optional dependencies or fixture isolation.
- Rule: Relative imports are not allowed.
- Rule: In production Python code under
src/g, import first-party modules rather than first-party members. - Good:
from g import types;types.Device - Bad:
from g.types import Device - Rule: Internal package initializers under
src/g/**/__init__.pyare package markers only. Do not define__all__, import/re-export submodules, assign aliases, or place helper functions there. - Rule: The top-level
src/g/__init__.pymay keep the lazy public entrypoint boundary required by the console script, but it must not define__all__. - Run
uv run python -m tooling.cli.debug tool.name=check_internal_init_exports(orjust check-internal-init-exports) before reviewing changes that touch package initializers.
Naming Conventions
- Rule: No abbreviations. No single-letter math variables. Use full, descriptive words.
- Bad: X, y, var, log_var, maf
- Good: design_matrix (or features), phenotypes (or targets), variance, log_variance, minor_allele_frequency
- Rule: No leading underscores for function or method names. We do not use pseudo-private indicators.
- Bad: def _calculate_variance():
- Good: def calculate_variance():
Type Annotations
- Rule: 100% Type Annotation Coverage.
- Types must pass the ty type checker without implicit Any fallbacks. Use exact types (e.g., jax.Array, np.ndarray).
- Rule: Finite sets of string values must use
enum.StrEnum, notstrannotations,Literal[...], or ad-hoc validation sets. This applies to configuration values, modes, formats, codecs, CLI choices, and any other closed choice domain. - Rule: Define an enum in the narrowest valid scope. If it is used in exactly one production file, define it in that file. If it is used in more than one production file, define it in
src/g/types.py. - Rule: Test-only enums should not live in production modules. If a test-only enum is used in one test file, define it in that test file. If it is shared across multiple test files, define it in
tests/types.py.
Return Types & Structured Data
- Rule: Never return bare tuples for multiple values. Use
@dataclass(frozen=True).
Bad:
def compute_regression(features: jax.Array, targets: jax.Array) -> tuple[jax.Array, jax.Array]:
return betas, standard_errors
Good:
from dataclasses import dataclass
import jax
@dataclass(frozen=True)
class RegressionResult:
"""Result of regression computation.
Attributes:
betas: Coefficient estimates.
standard\_errors: Standard errors of estimates.
"""
betas: jax.Array
standard\_errors: jax.Array
def compute_regression(features: jax.Array, targets: jax.Array) -> RegressionResult:
return RegressionResult(betas=betas, standard_errors=standard_errors)
JAX Pytree Containers
Internal Defaults
- Rule: Production Python code under
src/gmust not define default values on function or method parameters. - Rule: Dataclasses under
src/gmust not define field defaults ordefault_factoryvalues. - Pass optional values explicitly as
None, and pass policy constants explicitly at the call site. Defaults belong at config parsing, native boundary adaptation, or test/tooling helpers, not inside internal runtime code. -
Run
uv run python -m tooling.cli.debug tool.name=check_internal_defaults(orjust check-internal-defaults) before reviewing changes that touch internal signatures or dataclasses. -
Rule: For containers used inside JAX JIT boundaries (state containers, loop carriers), use
@dataclass(frozen=True)with@jax.tree_util.register_dataclass.
Good (for JIT-internal state):
from dataclasses import dataclass
import jax
@jax.tree_util.register_dataclass
@dataclass(frozen=True)
class LogisticState:
"""State for batched logistic IRLS.
Attributes:
coefficients: Current coefficient estimates.
converged\_mask: Boolean convergence mask.
iteration\_count: Iteration counter.
"""
coefficients: jax.Array
converged\_mask: jax.Array
iteration\_count: jax.Array
- Rule: For containers with mixed types (strings, booleans, numpy arrays alongside JAX arrays) that are NOT passed through JIT boundaries, use
@dataclass(frozen=True)without JAX registration.
Good (for I/O and metadata containers):
from dataclasses import dataclass
import jax
import numpy.typing as npt
import numpy as np
@dataclass(frozen=True)
class GenotypeChunk:
"""Genotype matrix with mixed metadata.
Attributes:
genotypes: JAX array of genotypes.
has\_missing\_values: Python boolean flag.
allele\_frequency: JAX array of frequencies.
"""
genotypes: jax.Array
has\_missing\_values: bool
allele\_frequency: jax.Array
- Note on NamedTuple: Avoid
typing.NamedTuple. While it supports unpacking and indexing,@dataclass(frozen=True)provides better consistency, IDE support, and explicit immutability. The codebase standardizes on dataclasses for all structured return types
Documentation
- Use the Google Python Style Guide format for docstrings.
- Rule: Omit types in docstrings. Since types are strictly enforced in the function signature, duplicating them in the docstring creates maintenance overhead.
- Detail Args:, Returns:, and Raises: with descriptions only.
PyO3 Native Stub Maintenance
- Follow Binding Layer Policy for all code under
src/binding. - Keep
src/g/_core.pyialigned with theg._core.cliregistrations undersrc/binding/. - Update the stub whenever Rust
#[pyclass],#[pyfunction],add_class, oradd_functionchanges touch argument or return types used by Python callers. - Run
uv run python -m tooling.cli.debug tool.name=check_pyo3_stub(orjust check-core-stub) before reviewing Rust/native-facing type updates. - Treat
just checkfailures fromcheck-core-stubas mandatory follow-up work when modifying native API exports.