Skip to content

Code Style

Python Version

JAFF requires Python 3.11 and higher (requires-python = ">=3.11" in pyproject.toml).

Start every module with from __future__ import annotations, then use built-in generics (list, dict, tuple) and PEP 604 unions (X | None) — not the legacy typing aliases.

# Good - built-in generics and PEP 604 unions
from __future__ import annotations

def process(items: list[str]) -> dict[str, int]:
    pass

def lookup(key: str) -> int | None:
    pass

# Avoid - legacy typing aliases
from typing import List, Dict, Optional

def process(items: List[str]) -> Optional[Dict[str, int]]:
    pass

Code Formatting

Use Ruff for both formatting and linting. The formatter config lives in pyproject.toml under [tool.ruff]:

  • Line length: 90 characters
  • Quote style: double quotes
  • Indent: 4 spaces

Run ruff format (it reads the config automatically — no need to pass flags):

# Format all code
ruff format src/ tests/

# Check formatting without modifying
ruff format --check src/

# Format specific file
ruff format src/jaff/network.py

Naming Conventions

Follow PEP 8 naming throughout.

Variables and Functions

Use snake_case:

# Good
user_name = "Alice"
def calculate_rate(temperature):
    pass

# Avoid
userName = "Alice"
def CalculateRate(temperature):
    pass

Classes

Use PascalCase:

# Good
class Network:
    pass

class CodeGenerator:
    pass

# Avoid
class network:
    pass

Constants

Use UPPER_SNAKE_CASE:

# Good
MAX_ITERATIONS = 1000
DEFAULT_TEMPERATURE = 300.0

# Avoid
maxIterations = 1000

Private Members

Prefix with underscore:

class Network:
    def __init__(self):
        self._internal_data = []  # Private
        self.public_data = []     # Public

    def _helper_method(self):  # Private
        pass

    def public_method(self):   # Public
        pass

Type Hints

Annotate every public function — both parameters and return type.

Always Use Type Hints

Untyped signatures hide intent and disable static checking. Always declare types.

# Good
def compute_rate(temperature: float, alpha: float) -> float:
    return alpha * temperature

# Avoid
def compute_rate(temperature, alpha):
    return alpha * temperature

Import Types

Use built-in generics and X | None. Only import from typing for names that have no built-in form (e.g. Any, TypedDict, cast, TYPE_CHECKING).

from __future__ import annotations

from pathlib import Path
from typing import Any

import numpy as np

def process_species(
    names: list[str],
    masses: np.ndarray,
    options: dict[str, Any] | None = None,
) -> tuple[list[str], np.ndarray]:
    pass

Complex Types

Name recurring or compound types with aliases to keep signatures readable.

from __future__ import annotations

# Type aliases for clarity
SpeciesDict = dict[str, int]
RateExpression = str | float

def load_network(
    filename: str,
    species_map: SpeciesDict | None = None,
) -> Network:
    pass

Docstrings

Every public module, class, and function gets a docstring.

NumPy Style

Use NumPy-style (numpydoc) docstrings with underlined sections (Parameters, Returns, Raises, Notes, Example). Cross-reference other objects with Sphinx roles — :class:, :meth:, :func: — so the rendered API docs link correctly.

def compute_rates(network: Network, temperature: float) -> np.ndarray:
    """Compute reaction rate coefficients.

    Calculates rate coefficients for all reactions in the network at the
    specified temperature using :meth:`~jaff.core.network.Network.calculate_rates`.

    Parameters
    ----------
    network : Network
        Chemical reaction network.
    temperature : float
        Gas temperature in Kelvin.

    Returns
    -------
    np.ndarray
        Array of rate coefficients in cm³/s.

    Raises
    ------
    ValueError
        If *temperature* is negative.

    Example
    -------
    >>> net = Network("network.dat")
    >>> rates = compute_rates(net, 100.0)
    >>> print(rates[0])
    1.2e-10
    """
    if temperature < 0:
        raise ValueError("Temperature must be non-negative")
    return network.calculate_rates(temperature)

Class Docstrings

Document constructor arguments in the class docstring under a Parameters section (not in __init__), matching :class:~jaff.codegen.codegen.Codegen.

class Codegen:
    """Multi-language code generator for chemical networks.

    Generates code for evaluating reaction rates, ODEs, and Jacobians in
    multiple target languages.

    Parameters
    ----------
    network : Network
        Parsed chemical reaction network.
    lang : str, optional
        Target language alias (``"c++"``, ``"c"``, ``"fortran"``, ``"python"``,
        …).  Default is ``"c++"``.

    Raises
    ------
    ValueError
        If *lang* is not a supported language.

    Example
    -------
    >>> net = Network("network.dat")
    >>> cg = Codegen(network=net, lang="c++")
    >>> rates = cg.get_rates_str(use_cse=True)
    """

    def __init__(self, network: Network, lang: str = "c++") -> None:
        ...

Module Docstrings

Open each module with a one-line summary followed by a short description. Reference the key classes the module exposes with :class: roles.

"""Chemical reaction network module.

Exposes the :class:`~jaff.core.network.Network` class for loading and managing
chemical reaction networks from various file formats.

Example
-------
>>> from jaff import Network
>>> net = Network("network.dat")
"""

Error Handling

Fail loudly with specific exceptions and descriptive messages.

Specific Exceptions

Catch the narrowest exception that applies — never a bare except:.

# Good
try:
    net = Network(filename)
except FileNotFoundError:
    print(f"File not found: {filename}")
except ValueError as e:
    print(f"Invalid network format: {e}")

# Avoid
try:
    net = Network(filename)
except:  # Too broad
    pass

Custom Exceptions

Define a domain exception hierarchy and raise it from a meaningful base.

class NetworkError(Exception):
    """Base exception for network errors."""
    pass

class ParseError(NetworkError):
    """Error parsing network file."""
    pass

def load_network(filename: str) -> Network:
    try:
        return Network(filename)
    except ValueError as e:
        raise ParseError(f"Failed to parse {filename}: {e}")

Error Messages

State what was expected and what was actually received.

# Good - descriptive
raise ValueError(f"Temperature must be positive, got {temp}")

# Avoid - vague
raise ValueError("Bad temperature")

Testing Style

See the Testing Guide for the full workflow; the rules below are style only.

Test Function Names

Name tests after the behaviour they verify, not test1/test2.

# Good - descriptive
def test_network_loads_krome_format():
    pass

def test_codegen_raises_error_for_invalid_language():
    pass

# Avoid
def test1():
    pass

Test Organization

Group related tests in a Test<Subject> class.

class TestNetwork:
    """Tests for Network class."""

    def test_load_from_file(self):
        """Test loading network from file."""
        pass

    def test_validate_species(self):
        """Test species validation."""
        pass

Assertions

Assert concrete values, not bare truthiness.

# Good - clear assertions
assert len(net.species) == 35
assert net.label == "react_COthin"
assert_almost_equal(rate, 1.2e-10, decimal=15)

# Avoid - unclear
assert x

Performance

Prefer the idiomatic, fast construct when it costs no clarity.

List Comprehensions

Build lists with comprehensions instead of append loops.

# Good - fast
species_names = [s.name for s in network.species]

# Slower
species_names = []
for s in network.species:
    species_names.append(s.name)

String Formatting

Use f-strings over concatenation.

# Good - f-strings (fast, readable)
message = f"Network has {n} species"

# Avoid - slow
message = "Network has " + str(n) + " species"

Best Practices

General Python habits the codebase follows.

Use Context Managers

Manage resources with with so they always close, even on error.

# Good
with open(filename, 'w') as f:
    f.write(content)

# Avoid
f = open(filename, 'w')
f.write(content)
f.close()

Use Pathlib

Use pathlib.Path for filesystem paths instead of os.path.

from pathlib import Path

# Good
path = Path("networks") / "react.dat"
if path.exists():
    content = path.read_text()

# Avoid
import os
path = os.path.join("networks", "react.dat")
if os.path.exists(path):
    with open(path) as f:
        content = f.read()

Early Returns

Return early to handle edge cases first and avoid deep nesting.

# Good - early return
def process(value: int | None) -> int:
    if value is None:
        return 0

    if value < 0:
        return -1

    return value * 2

# Avoid - nested if
def process(value):
    if value is not None:
        if value >= 0:
            return value * 2
        else:
            return -1
    else:
        return 0

Code Review Checklist

Before submitting code:

  • Code formatted with ruff
  • Sort imports with ruff
  • No linting errors (ruff)
  • Type hints added
  • Docstrings written
  • Tests added/updated
  • No commented-out code
  • No hardcoded paths
  • Error handling in place

See Also