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:
Constants
Use UPPER_SNAKE_CASE:
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