Code Style Guide¶
Version: 0.2.6 Last Updated: 2025-11-30
Overview¶
This guide documents Python-specific coding standards for the particula repository.
See Also: Code Culture - Development philosophy, code review practices, and the "smooth is safe, safe is fast" principle including the 100-line rule for PRs.
Language Version¶
Minimum Version: Python 3.9
Current Development Version: Python 3.13
From pyproject.toml:
requires-python = ">=3.9"
Naming Conventions¶
Functions/Methods: snake_case¶
Use lowercase with underscores for function and method names.
Example:
def calculate_density(mass: float, volume: float) -> float:
"""Calculate density from mass and volume."""
return mass / volume
def bat_activity_coefficients(molar_mass_ratio, organic_mole_fraction):
"""Calculate activity coefficients for the BAT model."""
pass
def convert_to_oh_equivalent(oxygen2carbon, molar_mass_ratio):
"""Convert oxygen to carbon ratio to OH equivalent."""
pass
Key Points:
- Use descriptive names that clearly indicate purpose
- Avoid abbreviations unless widely recognized (e.g., rh for relative humidity)
- Long names are okay if they improve clarity
Variables: snake_case¶
Use lowercase with underscores for variable names.
Example:
# Good
particle_diameter = 1.5e-6 # meters
molar_mass_ratio = 18.015 / 200.0
oxygen2carbon = 0.5
density_air = 1.184 # kg/m³
# Avoid
particleDiameter = 1.5e-6 # camelCase
MMR = 18.015 / 200.0 # unclear abbreviation
o2c = 0.5 # unclear abbreviation
Key Points: - Use full words for clarity - Include units in comments when applicable - Use descriptive names even for temporary variables
Constants: UPPER_CASE¶
Use uppercase with underscores for module-level constants.
Example:
# particula/util/constants.py
AVOGADRO_NUMBER = 6.02214076e23 # mol^-1
BOLTZMANN_CONSTANT = 1.380649e-23 # J/K
GAS_CONSTANT = 8.314462618 # J/(mol·K)
STANDARD_TEMPERATURE = 298.15 # K
STANDARD_PRESSURE = 101325.0 # Pa
Classes: PascalCase¶
Use PascalCase (CapitalizedWords) for class names.
Example:
class Aerosol:
"""Aerosol class combining gas and particle phases."""
pass
class AerosolBuilder:
"""Builder for creating Aerosol instances."""
pass
class FitValues(NamedTuple):
"""Named tuple for BAT model fit values."""
a1: List[float]
a2: List[float]
s: List[float]
Key Points: - No underscores in class names - Use descriptive names that indicate the class's purpose - Builder pattern: append "Builder" to the class name
Modules and Packages: snake_case¶
Use lowercase with underscores for module and package names.
Example:
particula/
├── activity_coefficients.py
├── bat_blending.py
├── vapor_pressure_strategies.py
├── atmosphere_builders.py
Key Points: - Keep module names short but descriptive - Use singular nouns unless the module contains a collection - Avoid dots in module names (use underscores instead)
Private Variables and Methods: _leading_underscore¶
Use a leading underscore for internal/private variables and methods.
Example:
class Calculator:
def __init__(self):
self._internal_cache = {} # Private instance variable
def _helper_method(self, x):
"""Internal helper method."""
return x * 2
def public_method(self, x):
"""Public API method."""
return self._helper_method(x) + 1
Type Variables: PascalCase¶
Use PascalCase for type variables.
Example:
from typing import TypeVar
T = TypeVar('T')
NumberType = TypeVar('NumberType', int, float)
Code Formatting¶
Line Length¶
Maximum: 80 characters
From pyproject.toml:
[tool.ruff]
line-length = 80
[tool.ruff.format]
docstring-code-line-length = 80
Break long lines:
# Good - function arguments broken across lines
def bat_activity_coefficients(
molar_mass_ratio: Union[float, NDArray[np.float64]],
organic_mole_fraction: Union[float, NDArray[np.float64]],
density: Union[float, NDArray[np.float64]],
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
"""Calculate activity coefficients."""
pass
# Good - long expressions broken with parentheses
result = (
long_variable_name_one
+ long_variable_name_two
+ long_variable_name_three
)
Indentation¶
Use 4 spaces per indentation level (never tabs).
def function():
if condition:
do_something()
if nested_condition:
do_nested_thing()
Blank Lines¶
- Two blank lines between top-level functions and classes
- One blank line between methods in a class
- One blank line to separate logical sections within functions (sparingly)
"""Module docstring."""
import numpy as np
CONSTANT_VALUE = 42
def first_function():
"""First function."""
pass
def second_function():
"""Second function."""
pass
class MyClass:
"""A class."""
def method_one(self):
"""Method one."""
pass
def method_two(self):
"""Method two."""
pass
Imports¶
Order (enforced by ruff): 1. Standard library imports 2. Third-party imports 3. Local application imports
Format:
# Standard library
import os
import sys
from typing import List, Optional, Tuple, Union
# Third-party
import numpy as np
from numpy.typing import NDArray
# Local
from particula.activity.bat_blending import bat_blending_weights
from particula.util.validate_inputs import validate_inputs
Key Points: - One import per line for explicit imports - Group imports with blank lines - Sort imports alphabetically within each group (ruff handles this) - Use absolute imports for particula modules
String Quotes¶
Preference: Double quotes "
From pyproject.toml:
[tool.ruff.format]
quote-style = "double" # implicit default
Example:
# Good
message = "Hello, world!"
docstring = """Multi-line docstring."""
# Also acceptable for avoiding escapes
message_with_quote = 'He said "Hello"'
Trailing Commas¶
Use trailing commas in multi-line structures (enforced by ruff format):
# Lists
values = [
1.0,
2.0,
3.0, # Trailing comma
]
# Function arguments
def function(
arg1: float,
arg2: float,
arg3: float, # Trailing comma
):
pass
Benefits: - Cleaner diffs when adding items - Prevents errors from missing commas
Type Hints¶
When to Use Type Hints¶
Always use type hints for: - Public function/method parameters - Public function/method return types - Class attributes (when not obvious)
Example:
from typing import Union, Optional, Tuple
from numpy.typing import NDArray
import numpy as np
def calculate_density(
mass: Union[float, NDArray[np.float64]],
volume: Union[float, NDArray[np.float64]],
) -> Union[float, NDArray[np.float64]]:
"""Calculate density from mass and volume.
Args:
mass: Mass in kg.
volume: Volume in m³.
Returns:
Density in kg/m³.
"""
return mass / volume
Type Hint Style¶
Use Union for multiple types:
# Good
value: Union[float, int]
array: Union[float, NDArray[np.float64]]
# Note: Python 3.10+ supports `float | int`, but we target 3.9+
Use Optional for None:
# Good
def process(data: Optional[np.ndarray] = None) -> float:
if data is None:
data = np.array([1.0, 2.0, 3.0])
return np.mean(data)
Use NDArray for numpy arrays:
from numpy.typing import NDArray
import numpy as np
def process_array(arr: NDArray[np.float64]) -> NDArray[np.float64]:
"""Process numpy array."""
return arr * 2.0
Relaxed Type Checking¶
From pyproject.toml:
[tool.ruff.lint]
extend-ignore = [
"ANN", # ignore all missing-type-*/missing-return-type checks
]
This means: - Type hints are encouraged but not strictly enforced - You can add type hints incrementally - Focus on public APIs first
Docstrings¶
Style: Google¶
From pyproject.toml:
[tool.ruff.lint.pydocstyle]
convention = "google"
See: Docstring Guide for comprehensive documentation standards.
Basic Structure¶
def function(arg1: float, arg2: str) -> bool:
"""Short one-line summary (imperative mood).
Optional longer description that provides more detail about what
the function does, how it works, or when to use it.
Args:
arg1: Description of arg1.
arg2: Description of arg2.
Returns:
Description of return value.
Raises:
ValueError: When arg1 is negative.
"""
if arg1 < 0:
raise ValueError("arg1 must be non-negative")
return arg1 > 0
Module Docstrings¶
Every module should have a docstring at the top:
"""Activity coefficients for organic-water mixtures.
Gorkowski, K., Preston, T. C., & Zuend, A. (2019).
Relative-humidity-dependent organic aerosol thermodynamics
Via an efficient reduced-complexity model.
Atmospheric Chemistry and Physics
https://doi.org/10.5194/acp-19-13383-2019
"""
import numpy as np
# ... rest of module
Comments¶
When to Comment¶
Do comment: - Complex algorithms or calculations - Non-obvious business logic - Workarounds for bugs or limitations - Citations for equations or methods - Units for physical quantities
Don't comment: - Obvious code (let the code speak) - Redundant docstrings
Comment Style¶
# Good - explains the "why"
# Use safe exponential to prevent overflow in BAT calculation
safe_exp = get_safe_exp()
result = safe_exp(large_value)
# Good - cites source
# From Gorkowski et al. (2019), Equation 7
blending_weight = (1.0 + np.exp(-s * (x - x0))) ** (-1)
# Good - includes units
temperature = 298.15 # K
pressure = 101325.0 # Pa
density = 1.184 # kg/m³
# Avoid - redundant
# Calculate density
density = mass / volume # This is obvious from the code
Inline Comments¶
Use sparingly, only when code logic is non-obvious:
# Good - clarifies non-obvious logic
if np.any(phase_sep_flag): # Check for phase separation
result = blended_value
else:
result = homogeneous_value
# Avoid - states the obvious
x = x + 1 # Increment x
Code Organization¶
Module Structure¶
Organize modules in this order:
- Module docstring
- Imports (standard lib, third-party, local)
- Module-level constants
- Module-level variables (if any)
- Functions (public first, then private)
- Classes (public first, then private)
Example:
"""Module for calculating particle properties.
Brief description of module purpose and any relevant citations.
"""
# Standard library
from typing import Union
# Third-party
import numpy as np
from numpy.typing import NDArray
# Local
from particula.util.constants import BOLTZMANN_CONSTANT
# Constants
DEFAULT_TEMPERATURE = 298.15 # K
# Public functions
def calculate_property(value: float) -> float:
"""Calculate property."""
return _helper(value)
# Private functions
def _helper(value: float) -> float:
"""Internal helper."""
return value * 2
# Public classes
class PropertyCalculator:
"""Calculator for properties."""
pass
File Organization¶
Keep modules focused and reasonably sized: - Prefer: Multiple small, focused modules - Avoid: Single large modules with many responsibilities
Example (from particula/activity):
activity/
├── activity_coefficients.py # BAT activity coefficients
├── bat_blending.py # Blending weights
├── bat_coefficients.py # Fit coefficients
├── convert_functional_group.py # Functional group conversion
├── gibbs.py # Gibbs free energy
├── gibbs_mixing.py # Gibbs mixing
├── phase_separation.py # Phase separation
Best Practices¶
Use Numpy for Numerical Computations¶
# Good - vectorized numpy operations
import numpy as np
def calculate_all(values: NDArray[np.float64]) -> NDArray[np.float64]:
"""Calculate for all values at once."""
return values ** 2 + 2 * values + 1
# Avoid - Python loops for array operations
def calculate_all_slow(values: list) -> list:
"""Slow version with loops."""
return [x**2 + 2*x + 1 for x in values]
Input Validation¶
Use the @validate_inputs decorator for public functions:
from particula.util.validate_inputs import validate_inputs
@validate_inputs({
"mass": "positive",
"volume": "positive",
"temperature": "nonnegative",
})
def calculate_density(
mass: float,
volume: float,
temperature: float = 298.15,
) -> float:
"""Calculate density with validation."""
return mass / volume
Available validators:
- "positive": Value must be > 0
- "nonnegative": Value must be >= 0
- "finite": Value must be finite (not inf or nan)
Avoid Mutable Default Arguments¶
# Bad
def append_to_list(item, items=[]):
items.append(item)
return items
# Good
def append_to_list(item, items=None):
if items is None:
items = []
items.append(item)
return items
Use Context Managers¶
# Good - file is always closed
with open("data.txt", "r") as f:
data = f.read()
# Avoid - file might not be closed on error
f = open("data.txt", "r")
data = f.read()
f.close()
Explicit is Better Than Implicit¶
# Good - clear and explicit
import numpy as np
array = np.array([1.0, 2.0, 3.0])
result = np.mean(array)
# Avoid - unclear what 'mean' is
from numpy import *
array = array([1.0, 2.0, 3.0])
result = mean(array)
Anti-Patterns to Avoid¶
Magic Numbers¶
# Bad
if temperature > 373.15:
print("Water is boiling")
# Good
WATER_BOILING_POINT = 373.15 # K at 1 atm
if temperature > WATER_BOILING_POINT:
print("Water is boiling")
Deeply Nested Code¶
# Bad
def process(data):
if data is not None:
if len(data) > 0:
if validate(data):
if transform(data):
return compute(data)
return None
# Good - early returns
def process(data):
if data is None:
return None
if len(data) == 0:
return None
if not validate(data):
return None
if not transform(data):
return None
return compute(data)
Unnecessary Comprehensions¶
# Bad - unnecessary list comprehension
sum([x**2 for x in values])
# Good - generator expression
sum(x**2 for x in values)
# Best - numpy for numerical operations
np.sum(values**2)
Testing Conventions¶
See: Testing Guide for comprehensive testing standards.
Key points:
- Test files: *_test.py suffix
- Test functions: test_ prefix
- Test classes: Test prefix
- Assertions allowed in test files (S101 ignored)
Summary¶
Key Style Requirements:
1. ✅ Use snake_case for functions, variables, modules
2. ✅ Use PascalCase for classes
3. ✅ Use UPPER_CASE for constants
4. ✅ Maximum line length: 80 characters
5. ✅ Google-style docstrings
6. ✅ Type hints for public APIs
7. ✅ Import order: stdlib → third-party → local
8. ✅ 4 spaces for indentation (never tabs)
9. ✅ Double quotes for strings
10. ✅ Validate inputs with @validate_inputs decorator
Quick Reference:
"""Module docstring."""
from typing import Union
import numpy as np
from particula.util import validate_inputs
CONSTANT_VALUE = 42.0
@validate_inputs({"mass": "positive", "volume": "positive"})
def calculate_density(
mass: Union[float, np.ndarray],
volume: Union[float, np.ndarray],
) -> Union[float, np.ndarray]:
"""Calculate density from mass and volume.
Args:
mass: Mass in kg.
volume: Volume in m³.
Returns:
Density in kg/m³.
"""
return mass / volume