Repository: calliope-project/calliope (language: python)

Issue:
debug functionality missing in Model.__init__
### What happened?

Calliope's `Model`  class initialization has a debug parameter with the following description:
```
            debug (bool, optional):
                If True, additional debug data will be included in the built model.
                Defaults to False.
```
At the moment, this parameter is completely unused by the code.

We check for `model_data.attrs["_debug_data"]` in other parts of the code, and for now it seems like this functionality won't work at all.



### Which operating systems have you used?

- [ ] macOS
- [ ] Windows
- [X] Linux

### Version

v0.7

### Relevant log output

_No response_


Discussion / hints:
Yeah, we need to purge that. Debug info is all available via the logger (and you can store that in a file following our tutorial: https://calliope.readthedocs.io/en/latest/examples/calliope_logging/)

Fix patch:
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..1b82a984
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# Documentation: https://EditorConfig.org
+# Inspired by Django .editorconfig file
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+
+[*.{yaml,yml}]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = true
+indent_size = 4
+
+[LICENSE]
+insert_final_newline = false
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 47cdd569..08e99401 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,16 +2,12 @@ default_language_version:
   python: python3
 
 repos:
-  - repo: https://github.com/psf/black
-    rev: 24.3.0
-    hooks:
-      - id: black
-
   - repo: https://github.com/astral-sh/ruff-pre-commit # https://beta.ruff.rs/docs/usage/#github-action
-    rev: v0.3.5
+    rev: v0.4.10
     hooks:
       - id: ruff
         args: [--fix, --exit-non-zero-on-fix]
+      - id: ruff-format
 
   - repo: https://github.com/codespell-project/codespell
     rev: v2.2.6
@@ -22,4 +18,4 @@ repos:
 
 ci:  # https://pre-commit.ci/
   autofix_prs: false
-  autoupdate_schedule: monthly
\ No newline at end of file
+  autoupdate_schedule: monthly
diff --git a/AUTHORS b/AUTHORS
index 2da331bf..70c3870b 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -12,3 +12,4 @@ Graeme Hawker, University of Strathclyde <graeme.hawker@strath.ac.uk>
 Francesco Lombardi, Politecnico di Milano <francesco.lombardi@polimi.it>
 Adriaan Hilbers, Imperial College London <a.hilbers17@imperial.ac.uk>
 Jann Launer, TU Delft <j.a.c.launer@tudelft.nl>
+Ivan Ruiz Manuel, TU Delft <i.ruizmanuel@tudelft.nl>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9525685..5fe9334f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@
 
 |fixed| Removed unused debug parameter in `Model.__init__()`
 
+|changed| Ruff linter checking was extended with pydocstrings and flake8-pytest.
+
+|changed| Moved from black formatting to the Ruff formatter (black-based, but faster).
+
 ## 0.7.0.dev3 (2024-02-14)
 
 ### User-facing changes
diff --git a/docs/contributing.md b/docs/contributing.md
index 6f6948b3..6475e0f4 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -196,13 +196,13 @@ Start reading our code and you'll get the hang of it.
 
 We mostly follow the official [Style Guide for Python Code (PEP8)](https://www.python.org/dev/peps/pep-0008/).
 
-We have chosen to use the uncompromising code formatter [`black`](https://github.com/psf/black/) and the linter [`ruff`](https://beta.ruff.rs/docs/).
+We have chosen to use [`ruff`](https://docs.astral.sh/ruff/) for code formatting and linting due to its fast speed and comprehensiveness.
 When run from the root directory of this repository, `pyproject.toml` should ensure that formatting and linting fixes are in line with our custom preferences (e.g., 88 character maximum line length).
-The philosophy behind using `black` is to have uniform style throughout the project dictated by code.
+The philosophy behind using the `ruff` linter is that it's based on [`black`](https://black.readthedocs.io/en/stable/), an uncompromising formatter that ensures uniform style throughout the project.
 Since `black` is designed to minimise diffs, and make patches more human readable, this also makes code reviews more efficient.
-To make this a smooth experience, you should run `pre-commit install` after setting up your development environment, so that `black` makes all the necessary fixes to your code each time you commit, and so that `ruff` will highlight any errors in your code.
-If you prefer, you can also set up your IDE to run these two tools whenever you save your files, and to have `ruff` highlight erroneous code directly as you type.
-Take a look at their documentation for more information on configuring this.
+To make this a smooth experience, you should run `pre-commit install` after setting up your development environment, so that `ruff` makes all the necessary formatting fixes to your code each time you commit, and so that it also highlights any style issues in your code.
+If you prefer, you can also set up your IDE to run `ruff` formatting whenever you save your files, and to have `ruff` highlight erroneous code directly as you type.
+Take a look at its documentation for more information on configuring this.
 
 We require all new contributions to have docstrings for all modules, classes and methods.
 When adding docstrings, we request you use the [Google docstring style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings).
diff --git a/docs/examples/loading_tabular_data.py b/docs/examples/loading_tabular_data.py
index 971dfc94..0611d2c2 100644
--- a/docs/examples/loading_tabular_data.py
+++ b/docs/examples/loading_tabular_data.py
@@ -717,7 +717,5 @@
 definition_matrix_old = (
     model_from_data_sources.inputs.definition_matrix.to_series().dropna()
 )
-definition_matrix_new = (
-    model_from_data_sources_w_deactivations.inputs.definition_matrix.to_series().dropna()
-)
+definition_matrix_new = model_from_data_sources_w_deactivations.inputs.definition_matrix.to_series().dropna()
 pd.concat([definition_matrix_old, definition_matrix_new], axis=1, keys=["old", "new"])
diff --git a/docs/hooks/add_notebooks.py b/docs/hooks/add_notebooks.py
index cc0f18cd..bb03ccf0 100644
--- a/docs/hooks/add_notebooks.py
+++ b/docs/hooks/add_notebooks.py
@@ -1,6 +1,7 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-"""
+"""Conversion of examples into Jupyter notebooks.
+
 Convert plaintext example notebooks to .ipynb format and store them as `notebook.ipynb`
 in every example notebook directory in the docs.
 """
@@ -18,7 +19,6 @@
 
 def on_files(files: list, config: dict, **kwargs):
     """Generate schema markdown reference sheets and attach them to the documentation."""
-
     for file in NOTEBOOK_DIR.glob("**/*.py"):
         if any(part.startswith(".") for part in file.parts):
             continue
diff --git a/docs/hooks/changelog_highlight.py b/docs/hooks/changelog_highlight.py
index e47cc96d..0b4d7e8f 100644
--- a/docs/hooks/changelog_highlight.py
+++ b/docs/hooks/changelog_highlight.py
@@ -1,3 +1,5 @@
+"""Changelog highlighting functionality."""
+
 REPLACEMENTS = {
     # Changelog
     "|new|": "<span class='bubble green'>new</span>",
@@ -13,6 +15,7 @@
 
 
 def on_page_content(html, **kwargs):
+    """Apply the global highlighting configuration to an html file."""
     for old, new in REPLACEMENTS.items():
         html = html.replace(old, new)
     return html
diff --git a/docs/hooks/generate_math_docs.py b/docs/hooks/generate_math_docs.py
index c2425a3c..d1e6a8db 100644
--- a/docs/hooks/generate_math_docs.py
+++ b/docs/hooks/generate_math_docs.py
@@ -1,8 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-"""
-Generate LaTeX math to include in the documentation.
-"""
+"""Generate LaTeX math to include in the documentation."""
 
 import importlib.resources
 import logging
@@ -41,6 +39,7 @@
 
 
 def on_files(files: list, config: dict, **kwargs):
+    """Process documentation for pre-defined calliope math files."""
     model_config = calliope.AttrDict.from_yaml(MODEL_PATH)
 
     base_model = generate_base_math_model()
@@ -82,6 +81,15 @@ def write_file(
     files: list[File],
     config: dict,
 ) -> None:
+    """Parse math files and produce markdown documentation.
+
+    Args:
+        filename (str): name of produced `.md` file.
+        description (str): first paragraph after title.
+        model (calliope.Model): calliope model with the given math.
+        files (list[File]): math files to parse.
+        config (dict): documentation configuration.
+    """
     title = model.inputs.attrs["name"] + " math"
 
     output_file = (Path("math") / filename).with_suffix(".md")
@@ -128,7 +136,7 @@ def write_file(
 
 
 def generate_base_math_model() -> calliope.Model:
-    """Generate model with documentation for the base math
+    """Generate model with documentation for the base math.
 
     Args:
         model_config (dict): Calliope model config.
@@ -144,27 +152,16 @@ def generate_base_math_model() -> calliope.Model:
 def generate_custom_math_model(
     base_model: calliope.Model, override: str
 ) -> calliope.Model:
-    """Generate model with documentation for a pre-defined math file, showing only the changes made
-    relative to the base math.
+    """Generate model with documentation for a pre-defined math file.
+
+    Only the changes made relative to the base math will be shown.
 
     Args:
         base_model (calliope.Model): Calliope model with only the base math applied.
         override (str): Name of override to load from the list available in the model config.
     """
     model = calliope.Model(model_definition=MODEL_PATH, scenario=override)
-    _keep_only_changes(base_model, model)
-
-    return model
-
 
-def _keep_only_changes(base_model: calliope.Model, model: calliope.Model) -> None:
-    """Compare custom math model with base model and keep only the math strings that are
-    different between the two. Changes are made in-place in the custom math model docs.
-
-    Args:
-        base_model (calliope.Model): Calliope model with base math applied.
-        model (calliope.Model): Calliope model with custom math applied.
-    """
     full_del = []
     expr_del = []
     for component_group, component_group_dict in model.math.items():
@@ -196,7 +193,9 @@ def _keep_only_changes(base_model: calliope.Model, model: calliope.Model) -> Non
         model.math_documentation._instance._dataset["carrier_in"].attrs["references"]
     )
 
+    return model
+
 
 def _add_to_description(component_dict: dict, update_string: str) -> None:
-    "Prepend the math component description"
+    """Prepend the math component description."""
     component_dict["description"] = f"{update_string}\n{component_dict['description']}"
diff --git a/docs/hooks/generate_math_examples.py b/docs/hooks/generate_math_examples.py
index 8e41ce52..b7c14d73 100644
--- a/docs/hooks/generate_math_examples.py
+++ b/docs/hooks/generate_math_examples.py
@@ -1,8 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-"""
-Generates a markdown file listing all custom math examples.
-"""
+"""Generates a markdown file listing all custom math examples."""
 
 import tempfile
 from io import StringIO
@@ -37,7 +35,6 @@
 
 def on_files(files: list, config: dict, **kwargs):
     """Generate custom math example markdown files and attach them to the documentation and the navigation tree."""
-
     # Find the navigation tree list that we will populate with reference to new markdown files
     top_level_nav_reference = [
         idx
diff --git a/docs/hooks/generate_plots.py b/docs/hooks/generate_plots.py
index 31d4da5c..9a606adc 100644
--- a/docs/hooks/generate_plots.py
+++ b/docs/hooks/generate_plots.py
@@ -1,8 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-"""
-Generate interactive plots to use within the docs
-"""
+"""Generate interactive plots to use within the docs."""
 
 import tempfile
 from pathlib import Path
@@ -18,7 +16,6 @@
 
 def on_files(files: list, config: dict, **kwargs):
     """Generate schema markdown reference sheets and attach them to the documentation."""
-
     file_obj = _generate_front_page_timeseries_plot(config)
     files.append(file_obj)
 
@@ -140,12 +137,11 @@ def _get_net_flows(model: calliope.Model, **sel) -> pd.DataFrame:
 
     Args:
         model (calliope.Model): Calliope model with results.
-    Keyword Args:
-        Index items on which to slice the flows in xarray before summing over nodes and renaming techs.
+        **sel: Index items on which to slice the flows in xarray before summing over nodes and renaming techs.
+
     Returns:
         pd.DataFrame: Net-flow timeseries tidy dataframe.
     """
-
     if "techs" in sel.keys():
         names = model.inputs.name.sel(techs=sel["techs"])
     else:
diff --git a/docs/hooks/generate_readable_schema.py b/docs/hooks/generate_readable_schema.py
index 2a545964..19ee6d32 100644
--- a/docs/hooks/generate_readable_schema.py
+++ b/docs/hooks/generate_readable_schema.py
@@ -1,8 +1,10 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
 
-"""
-Converts YAML schema to a readable markdown file with nested lists representing the schema properties.
+"""Schema (a.k.a. "properties") documentation functionality.
+
+Converts YAML schema to a readable markdown file with nested lists representing the
+schema properties.
 """
 
 import tempfile
@@ -30,11 +32,15 @@ def on_files(files: list, config: dict, **kwargs):
 
 
 def _schema_to_md(schema: dict, filename: str, config: dict) -> File:
-    """Parse the schema to markdown, edit some lines, then dump to file
+    """Parse the schema to markdown, edit some lines, then dump to file.
 
     Args:
         schema (dict): Path to the YAML schema to be converted.
-        path_to_md (Path): Path to Markdownfile to dump the converted schema.
+        filename (str): Path to Markdownfile to dump the converted schema.
+        config (dict): Documentation configuration.
+
+    Returns:
+        File: markdown (.md) file.
     """
     output_file = (Path("reference") / filename).with_suffix(".md")
     output_full_filepath = Path(TEMPDIR.name) / output_file
@@ -43,8 +49,11 @@ def _schema_to_md(schema: dict, filename: str, config: dict) -> File:
     parser = jsonschema2md.Parser()
     parser.tab_size = 4
 
+    # We don't want to represent the schema as a schema,
+    # so we remove parts of the generated markdown that refers to it as such
     lines = parser.parse_schema(schema)
-    lines = _customise_markdown(lines)
+    assert lines[2] == "## Properties\n\n"
+    del lines[2]
 
     output_full_filepath.write_text("\n".join(lines))
 
@@ -54,13 +63,3 @@ def _schema_to_md(schema: dict, filename: str, config: dict) -> File:
         dest_dir=config["site_dir"],
         use_directory_urls=config["use_directory_urls"],
     )
-
-
-def _customise_markdown(lines: list[str]) -> list[str]:
-    """
-    We don't want to represent the schema as a schema, so we remove parts of the generated markdown that refers to it as such.
-    """
-    # 1. Remove subheadline
-    assert lines[2] == "## Properties\n\n"
-    del lines[2]
-    return lines
diff --git a/docs/hooks/macros.py b/docs/hooks/macros.py
index 33015171..b41511f5 100644
--- a/docs/hooks/macros.py
+++ b/docs/hooks/macros.py
@@ -1,14 +1,14 @@
+"""Documentation hook macros."""
+
 import pandas as pd
 
 
 def define_env(env):
-    "Hook function"
+    """Hook function."""
 
     @env.macro
     def read_csv(file, **kwargs):
-        """
-        Read a CSV file and render it as a HTML table.
-        """
+        """Read a CSV file and render it as a HTML table."""
         styles = [
             # table properties
             dict(
diff --git a/pyproject.toml b/pyproject.toml
index 69db440a..81fcce55 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,56 +30,40 @@ directory = "reports/coverage"
 [tool.coverage.xml]
 output = "reports/coverage/coverage.xml"
 
-[tool.black]
-line-length = 88
-skip-magic-trailing-comma = true
-target-version = ['py310', 'py311', 'py312']
-include = '\.pyi?$'
-exclude = '''
-/(
-    \.eggs
-  | \.git
-  | \.github
-  | \.mypy_cache
-  | \.pytest_cache
-  | \.vscode
-  | _build
-  | build
-  | dist
-  | .*\.egg-info
-
-  # directories without python source files
-  | requirements
-)/
-'''
-
 [tool.ruff]
 line-length = 88
-select = ["E", "F", "I", "Q", "W"]
+
+[tool.ruff.format]
+exclude = [".*.egg-info", "requirements/**"]
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "Q", "W", "D", "PT"]
 # line too long; Black will handle these
 ignore = ["E501"]
 
-[tool.ruff.mccabe]
+[tool.ruff.lint.mccabe]
 # Unlike Flake8, default to a complexity level of 10.
 max-complexity = 10
 
 # Ignore `E402` (import violations) and `F401` (unused imports) in all `__init__.py` files
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint.per-file-ignores]
 "__init__.py" = ["E402", "F401"]
 "*.ipynb" = ["E402"]
+"tests/*" = ["D"]
+"docs/examples/*" = ["D"]
 
-[tool.ruff.flake8-quotes]
+[tool.ruff.lint.flake8-quotes]
 docstring-quotes = "double"
 
-[tool.ruff.pydocstyle]
+[tool.ruff.lint.pydocstyle]
 convention = "google"
 
-[tool.ruff.pycodestyle]
+[tool.ruff.lint.pycodestyle]
 max-doc-length = 200
 ignore-overlong-task-comments = true
 
 [tool.codespell]
-skip = 'tests/**/*,**/*.ipynb,doc/**/*'
+skip = 'tests/**/*,**/*.ipynb,doc/**/*,AUTHORS'
 count = ''
 quiet-level = 3
 
diff --git a/src/calliope/__init__.py b/src/calliope/__init__.py
index a7e3601d..794ebc82 100644
--- a/src/calliope/__init__.py
+++ b/src/calliope/__init__.py
@@ -1,3 +1,5 @@
+"""Public packaging of Calliope."""
+
 from calliope import examples, exceptions
 from calliope._version import __version__
 from calliope.attrdict import AttrDict
diff --git a/src/calliope/attrdict.py b/src/calliope/attrdict.py
index 65c26f2d..f8802dbd 100644
--- a/src/calliope/attrdict.py
+++ b/src/calliope/attrdict.py
@@ -1,9 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-Implements the AttrDict class (a subclass of regular dict) used for managing model configuration.
-"""
+"""AttrDict implementation (a subclass of regular dict) used for managing model configuration."""
 
 import copy
 import io
@@ -29,7 +26,7 @@ def __nonzero__(self):
 
 
 def _yaml_load(src):
-    """Load YAML from a file object or path with useful parser errors"""
+    """Load YAML from a file object or path with useful parser errors."""
     yaml = ruamel_yaml.YAML(typ="safe")
     if not isinstance(src, str):
         try:
@@ -52,16 +49,7 @@ def _yaml_load(src):
 
 
 class AttrDict(dict):
-    """
-    A subclass of ``dict`` with key access by attributes::
-
-        d = AttrDict({'a': 1, 'b': 2})
-        d.a == 1  # True
-
-    Includes a range of additional methods to read and write to YAML,
-    and to deal with nested keys.
-
-    """
+    """Extended `dict` class."""
 
     __name__ = "AttrDict"
 
@@ -69,6 +57,15 @@ class AttrDict(dict):
     __setattr__ = dict.__setitem__
 
     def __init__(self, source_dict=None):
+        """A subclass of ``dict`` with key access by attributes.
+
+        Examples:
+            d = AttrDict({'a': 1, 'b': 2})
+            d.a == 1  # True
+
+        Includes a range of additional methods to read and write to YAML,
+        and to deal with nested keys.
+        """
         super().__init__()
 
         if source_dict is not None:
@@ -78,18 +75,17 @@ def __init__(self, source_dict=None):
                 raise ValueError("Must pass a dict to AttrDict")
 
     def copy(self):
-        """Override copy method so that it returns an AttrDict"""
+        """Override copy method so that it returns an AttrDict."""
         return AttrDict(self.as_dict().copy())
 
     def __deepcopy__(self, memo):
-        """Override copy method so that it returns an AttrDict"""
+        """Override copy method so that it returns an AttrDict."""
         return AttrDict(copy.deepcopy(self.as_dict(), memo))
 
     def init_from_dict(self, d):
-        """
-        Initialize a new AttrDict from the given dict. Handles any
-        nested dicts by turning them into AttrDicts too::
+        """Initialize a new AttrDict from the given dict.
 
+        Handles any nested dicts by turning them into AttrDicts too:
             d = AttrDict({'a': 1, 'b': {'x': 1, 'y': 2}})
             d.b.x == 1  # True
 
@@ -158,24 +154,24 @@ def _resolve_imports(
     def from_yaml(
         cls, filename: str | Path, resolve_imports: bool | str = True
     ) -> Self:
-        """
-        Returns an AttrDict initialized from the given path or
-        file object ``f``, which must point to a YAML file. The path can
-        be a string or a pathlib.Path.
+        """Returns an AttrDict initialized from the given path or file path.
+
+        If `resolve_imports` is True, top-level `import:` statements
+        are resolved recursively.
+        If `resolve_imports` is False, top-level `import:` statements
+        are treated like any other key and not further processed.
+        If `resolve_imports` is a string, such as `foobar`, import
+        statements underneath that key are resolved, i.e. `foobar.import:`.
+        When resolving import statements, anything defined locally
+        overrides definitions in the imported file.
 
         Args:
-            filename (str | pathlib.Path):
-            resolve_imports (bool | str, optional):
-                If ``resolve_imports`` is True, top-level ``import:`` statements
-                are resolved recursively.
-                If ``resolve_imports is False, top-level ``import:`` statements
-                are treated like any other key and not further processed.
-                If ``resolve_imports`` is a string, such as ``foobar``, import
-                statements underneath that key are resolved, i.e. ``foobar.import:``.
-                When resolving import statements, anything defined locally
-                overrides definitions in the imported file.
+            filename (str | Path): YAML file.
+            resolve_imports (bool | str, optional): top-level `import:` solving option.
+                Defaults to True.
+
         Returns:
-            calliope.AttrDict:
+            Self: constructed AttrDict
         """
         filename = Path(filename)
         loaded = cls(_yaml_load(filename.read_text(encoding="utf-8")))
@@ -184,8 +180,7 @@ def from_yaml(
 
     @classmethod
     def from_yaml_string(cls, string: str, resolve_imports: bool | str = True) -> Self:
-        """
-        Returns an AttrDict initialized from the given string.
+        """Returns an AttrDict initialized from the given string.
 
         Input string must be valid YAML.
 
@@ -200,6 +195,7 @@ def from_yaml_string(cls, string: str, resolve_imports: bool | str = True) -> Se
                 statements underneath that key are resolved, i.e. ``foobar.import:``.
                 When resolving import statements, anything defined locally
                 overrides definitions in the imported file.
+
         Returns:
             calliope.AttrDict:
 
@@ -209,10 +205,9 @@ def from_yaml_string(cls, string: str, resolve_imports: bool | str = True) -> Se
         return loaded
 
     def set_key(self, key, value):
-        """
-        Set the given ``key`` to the given ``value``. Handles nested
-        keys, e.g.::
+        """Set the given ``key`` to the given ``value``.
 
+        Handles nested keys, e.g.:
             d = AttrDict()
             d.set_key('foo.bar', 1)
             d.foo.bar == 1  # True
@@ -243,13 +238,12 @@ def set_key(self, key, value):
                 self[key] = value
 
     def get_key(self, key, default=_MISSING):
-        """
-        Looks up the given ``key``. Like set_key(), deals with nested
-        keys.
+        """Looks up the given ``key``.
+
+        Deals with nested keys.
 
         If default is anything but ``_MISSING``, the given default is
         returned if the key does not exist.
-
         """
         if "." in key:
             # Nested key of form "foo.bar"
@@ -290,17 +284,14 @@ def del_key(self, key):
             del self[key]
 
     def as_dict(self, flat=False):
-        """
-        Return the AttrDict as a pure dict (with nested dicts if
-        necessary).
-
-        """
+        """Return the AttrDict as a pure dict (with nested dicts if necessary)."""
         if flat:
             return self.as_dict_flat()
         else:
             return self.as_dict_nested()
 
     def as_dict_nested(self):
+        """Return the AttrDict as a pure dict, converting nested AttrDicts."""
         d = {}
         for k, v in self.items():
             if isinstance(v, AttrDict):
@@ -312,6 +303,7 @@ def as_dict_nested(self):
         return d
 
     def as_dict_flat(self):
+        """Return a flat dictionary."""
         d = {}
         keys = self.keys_nested()
         for k in keys:
@@ -319,10 +311,10 @@ def as_dict_flat(self):
         return d
 
     def to_yaml(self, path=None):
-        """
-        Saves the AttrDict to the ``path`` as a YAML file, or returns
-        a YAML string if ``path`` is None.
+        """Conversion to YAML.
 
+        Saves the AttrDict to the ``path`` as a YAML file or returns a YAML string
+        if ``path`` is None.
         """
         result = self.copy()
         yaml_ = ruamel_yaml.YAML()
@@ -354,9 +346,9 @@ def to_yaml(self, path=None):
             return stream.getvalue()
 
     def keys_nested(self, subkeys_as="list"):
-        """
-        Returns all keys in the AttrDict, including the keys of
-        nested subdicts (which may be either regular dicts or AttrDicts).
+        """Returns all keys in the AttrDict, including nested keys.
+
+        Nested subdicts may be either regular dicts or AttrDicts.
 
         If ``subkeys_as='list'`` (default), then a list of
         all keys is returned, in the form ``['a', 'b.b1', 'b.b2']``.
@@ -380,27 +372,25 @@ def keys_nested(self, subkeys_as="list"):
 
     def union(
         self,
-        other,
-        allow_override=False,
-        allow_replacement=False,
-        allow_subdict_override_with_none=False,
+        other: Self | dict,
+        allow_override: bool = False,
+        allow_replacement: bool = False,
+        allow_subdict_override_with_none: bool = False,
     ):
-        """
-        Merges the AttrDict in-place with the passed ``other``
-        AttrDict. Keys in ``other`` take precedence, and nested keys
-        are properly handled.
-
-        If ``allow_override`` is False, a KeyError is raised if
-        other tries to redefine an already defined key.
-
-        If ``allow_replacement``, allow "_REPLACE_" key to replace an
-        entire sub-dict.
-
-        If ``allow_subdict_override_with_none`` is False (default),
-        a key of the form ``this.that: None`` in other will be ignored
-        if subdicts exist in self like ``this.that.foo: 1``, rather
-        than wiping them.
+        """In-place merge with another AttrDict.
 
+        Args:
+            other (Self | dict): other AttrDict to use in union. Takes precedence.
+            allow_override (bool, optional): whether or not to allow overrides of
+                already defined keys. Defaults to False.
+            allow_replacement (bool, optional): allow "_REPLACE_" key to replace an
+                entire sub-dict. Defaults to False.
+            allow_subdict_override_with_none (bool, optional): if False, keys in the
+                form `this.that: None` in `other` will be ignored if subdicts exist in
+                self like `this.that.foo: 1`, rather than wiping them. Defaults to False.
+
+        Raises:
+            KeyError: `other` has an already defined key and `allow_override == False`
         """
         self_keys = self.keys_nested()
         other_keys = other.keys_nested()
diff --git a/src/calliope/backend/__init__.py b/src/calliope/backend/__init__.py
index e69de29b..fe8e8c0e 100644
--- a/src/calliope/backend/__init__.py
+++ b/src/calliope/backend/__init__.py
@@ -0,0 +1,1 @@
+"""Callipe's backend module."""
diff --git a/src/calliope/backend/backend_model.py b/src/calliope/backend/backend_model.py
index d9ff3532..b871bb57 100644
--- a/src/calliope/backend/backend_model.py
+++ b/src/calliope/backend/backend_model.py
@@ -1,9 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-Methods to interface with the optimisation problem
-"""
+"""Methods to interface with the optimisation problem."""
 
 from __future__ import annotations
 
@@ -57,6 +54,8 @@
 
 
 class BackendModelGenerator(ABC):
+    """Helper class for backends."""
+
     _VALID_COMPONENTS: tuple[_COMPONENTS_T, ...] = typing.get_args(_COMPONENTS_T)
     _COMPONENT_ATTR_METADATA = ["description", "unit", "default"]
 
@@ -68,8 +67,8 @@ def __init__(self, inputs: xr.Dataset, **kwargs):
 
         Args:
             inputs (xr.Dataset): Calliope model data.
+            **kwargs (Any): build configuration overrides.
         """
-
         self._dataset = xr.Dataset()
         self.inputs = inputs.copy()
         self.inputs.attrs = deepcopy(inputs.attrs)
@@ -88,8 +87,8 @@ def add_parameter(
         default: Any = np.nan,
         use_inf_as_na: bool = False,
     ) -> None:
-        """
-        Add input parameter to backend model in-place.
+        """Add input parameter to backend model in-place.
+
         If the backend interface allows for mutable parameter objects, they will be
         generated, otherwise a copy of the model input dataset will be used.
         In either case, NaN values are filled with the given parameter default value.
@@ -97,21 +96,19 @@ def add_parameter(
         Args:
             parameter_name (str): Name of parameter.
             parameter_values (xr.DataArray): Array of parameter values.
-            default (Any, optional):
-                Default value to fill NaN entries in parameter values array.
-                Defaults to np.nan.
-            use_inf_as_na (bool, optional):
-                If True, will consider np.inf parameter value entries as np.nan and
-                consequently try to fill those entries with the parameter default value.
-                Defaults to False.
+            default (Any, optional): default value to fill NaN entries in parameter
+                values array. Defaults to np.nan.
+            use_inf_as_na (bool, optional): If True, will consider np.inf parameter
+                value entries as np.nan and consequently try to fill those entries
+                with the parameter default value. Defaults to False.
         """
 
     @abstractmethod
     def add_constraint(
         self, name: str, constraint_dict: parsing.UnparsedConstraintDict
     ) -> None:
-        """
-        Add constraint equation to backend model in-place.
+        """Add constraint equation to backend model in-place.
+
         Resulting backend dataset entries will be constraint objects.
 
         Args:
@@ -125,46 +122,39 @@ def add_constraint(
     def add_global_expression(
         self, name: str, expression_dict: parsing.UnparsedExpressionDict
     ) -> None:
-        """
-        Add global expression (arithmetic combination of parameters and/or decision variables)
-        to backend model in-place.
+        """Add global expression (arithmetic combination of parameters and/or decision variables) to backend model in-place.
+
         Resulting backend dataset entries will be linear expression objects.
 
         Args:
-            name (str):
-                Name of the global expression
-            expression_dict (parsing.UnparsedExpressionDict):
-                Global expression configuration dictionary, ready to be parsed and then evaluated.
+            name (str): name of the global expression
+            expression_dict (parsing.UnparsedExpressionDict): global expression configuration dictionary, ready to be parsed and then evaluated.
         """
 
     @abstractmethod
     def add_variable(
         self, name: str, variable_dict: parsing.UnparsedVariableDict
     ) -> None:
-        """
-        Add decision variable to backend model in-place.
+        """Add decision variable to backend model in-place.
+
         Resulting backend dataset entries will be decision variable objects.
 
         Args:
-            name (str):
-                Name of the variable.
-            variable_dict (parsing.UnparsedVariableDict):
-                Variable configuration dictionary, ready to be parsed and then evaluated.
+            name (str): name of the variable.
+            variable_dict (parsing.UnparsedVariableDict): unparsed variable configuration dictionary.
         """
 
     @abstractmethod
     def add_objective(
         self, name: str, objective_dict: parsing.UnparsedObjectiveDict
     ) -> None:
-        """
-        Add objective arithmetic to backend model in-place.
+        """Add objective arithmetic to backend model in-place.
+
         Resulting backend dataset entry will be a single, unindexed objective object.
 
         Args:
-            name (str):
-                Name of the objective.
-            objective_dict (parsing.UnparsedObjectiveDict):
-                Objective configuration dictionary, ready to be parsed and then evaluated.
+            name (str): name of the objective.
+            objective_dict (parsing.UnparsedObjectiveDict): unparsed objective configuration dictionary.
         """
 
     def log(
@@ -174,11 +164,13 @@ def log(
         message: str,
         level: Literal["info", "warning", "debug", "error", "critical"] = "debug",
     ):
-        """Log to module-level logger with some prettification of the message
+        """Log to module-level logger with some prettification of the message.
 
         Args:
-            message (str): Message to log.
-            level (Literal["info", "warning", "debug", "error", "critical"], optional): Log level. Defaults to "debug".
+            component_type (_COMPONENTS_T): type of component.
+            component_name (str): name of the component.
+            message (str): message to log.
+            level (Literal["info", "warning", "debug", "error", "critical"], optional): log level. Defaults to "debug".
         """
         getattr(LOGGER, level)(
             f"Optimisation model | {component_type}:{component_name} | {message}"
@@ -231,8 +223,7 @@ def _build(self) -> None:
             LOGGER.info(f"Optimisation Model | {components} | Generated.")
 
     def _add_run_mode_math(self) -> None:
-        """If not given in the add_math list, override model math with run mode math"""
-
+        """If not given in the add_math list, override model math with run mode math."""
         # FIXME: available modes should not be hardcoded here. They should come from a YAML schema.
         mode = self.inputs.attrs["config"].build.mode
         add_math = self.inputs.attrs["applied_additional_math"]
@@ -264,20 +255,20 @@ def _add_component(
         """Generalised function to add a optimisation problem component array to the model.
 
         Args:
-            name:
-                Name of the component.
-                If not providing the `component_dict` directly, this name must be available in the input math provided on initialising the class.
-            component_dict (Union[parsing.UnparsedConstraintDict, parsing.UnparsedExpressionDict, parsing.UnparsedObjectiveDict, parsing.UnparsedVariableDict]):
-                Unparsed YAML dictionary configuration.
-            component_setter (Callable):
-                Function to combine evaluated xarray DataArrays into backend component objects.
-                Will receive outputs of `evaluate_where` and (optionally, if `equations` is a component dictionary key) `evaluate_expression` as inputs.
-            component_type (Literal["variables", "global_expressions", "constraints", "objectives"])
+            name (str): name of the component. If not providing the `component_dict` directly,
+                this name must be available in the input math provided on initialising the class.
+            component_dict (Optional[Tp]): unparsed YAML dictionary configuration.
+            component_setter (Callable): function to combine evaluated xarray DataArrays into backend component objects.
+            component_type (Literal["variables", "global_expressions", "constraints", "objectives"]):
+                type of the added component.
+            break_early (bool, optional): break if the component is not active. Defaults to True.
 
         Raises:
-            BackendError:
-                The sub-equations of the parsed component cannot generate component
+            BackendError: The sub-equations of the parsed component cannot generate component
                 objects on duplicate index entries.
+
+        Returns:
+            Optional[parsing.ParsedBackendComponent]: parsed component. None if the break_early condition was met.
         """
         references: set[str] = set()
 
@@ -363,7 +354,8 @@ def delete_component(self, key: str, component_type: _COMPONENTS_T) -> None:
     @abstractmethod
     def _create_obj_list(self, key: str, component_type: _COMPONENTS_T) -> None:
         """Attach an empty list object to the backend model object.
-        This may be a backend-specific subclass of a standard list object.
+
+        The attachment may be a backend-specific subclass of a standard list object.
 
         Args:
             key (str): Name of object.
@@ -374,8 +366,8 @@ def _create_obj_list(self, key: str, component_type: _COMPONENTS_T) -> None:
         """
 
     def _add_all_inputs_as_parameters(self) -> None:
-        """
-        Add all parameters to backend dataset in-place.
+        """Add all parameters to backend dataset in-place.
+
         If model data does not include a parameter, their default values will be added here
         as unindexed backend dataset parameters.
 
@@ -408,10 +400,7 @@ def _add_all_inputs_as_parameters(self) -> None:
 
     @staticmethod
     def _clean_arrays(*args) -> None:
-        """
-        Preemptively delete objects with large memory footprints that might otherwise
-        stick around longer than necessary.
-        """
+        """Preemptively delete of objects with large memory footprints."""
         del args
 
     def _add_to_dataset(
@@ -422,8 +411,7 @@ def _add_to_dataset(
         unparsed_dict: Union[parsing.UNPARSED_DICTS, dict],
         references: Optional[set] = None,
     ):
-        """
-        Add array of backend objects to backend dataset in-place.
+        """Add array of backend objects to backend dataset in-place.
 
         Args:
             name (str): Name of entry in dataset.
@@ -438,7 +426,6 @@ def _add_to_dataset(
                 All referenced objects will have their "references" attribute updated with this object's name.
                 Defaults to None.
         """
-
         add_attrs = {
             attr: unparsed_dict.pop(attr)
             for attr in self._COMPONENT_ATTR_METADATA
@@ -467,8 +454,7 @@ def _add_to_dataset(
     def _apply_func(
         self, func: Callable, *args, output_core_dims: tuple = ((),), **kwargs
     ) -> xr.DataArray:
-        """
-        Apply a function to every element of an arbitrary number of xarray DataArrays.
+        """Apply a function to every element of an arbitrary number of xarray DataArrays.
 
         Args:
             func (Callable):
@@ -500,19 +486,18 @@ def _apply_func(
         )
 
     def _raise_error_on_preexistence(self, key: str, obj_type: _COMPONENTS_T):
-        """
+        """Detect if preexistance errors are present the dataset.
+
         We do not allow any overlap of backend object names since they all have to
-        co-exist in the backend dataset.
-        I.e., users cannot overwrite any backend component with another
-        (of the same type or otherwise).
+        co-exist in the backend dataset. I.e., users cannot overwrite any backend
+        component with another (of the same type or otherwise).
 
         Args:
             key (str): Backend object name
             obj_type (Literal["variables", "constraints", "objectives", "parameters", "expressions"]): Object type.
 
         Raises:
-            BackendError:
-                Raised if `key` already exists in the backend model
+            BackendError: if `key` already exists in the backend model
                 (either with the same or different type as `obj_type`).
         """
         if key in self._dataset.keys():
@@ -529,31 +514,37 @@ def _raise_error_on_preexistence(self, key: str, obj_type: _COMPONENTS_T):
 
     @property
     def constraints(self):
-        "Slice of backend dataset to show only built constraints"
+        """Slice of backend dataset to show only built constraints."""
         return self._dataset.filter_by_attrs(obj_type="constraints")
 
     @property
     def variables(self):
-        "Slice of backend dataset to show only built variables"
+        """Slice of backend dataset to show only built variables."""
         return self._dataset.filter_by_attrs(obj_type="variables")
 
     @property
     def parameters(self):
-        "Slice of backend dataset to show only built parameters"
+        """Slice of backend dataset to show only built parameters."""
         return self._dataset.filter_by_attrs(obj_type="parameters")
 
     @property
     def global_expressions(self):
-        "Slice of backend dataset to show only built global expressions"
+        """Slice of backend dataset to show only built global expressions."""
         return self._dataset.filter_by_attrs(obj_type="global_expressions")
 
     @property
     def objectives(self):
-        "Slice of backend dataset to show only built objectives"
+        """Slice of backend dataset to show only built objectives."""
         return self._dataset.filter_by_attrs(obj_type="objectives")
 
     @property
-    def valid_component_names(self):
+    def valid_component_names(self) -> set:
+        """Return a set of valid component names in the model data.
+
+        Returns:
+            set: set of valid names.
+        """
+
         def _filter(val):
             return val in ["variables", "parameters", "global_expressions"]
 
@@ -567,12 +558,15 @@ def _filter(val):
 
 
 class BackendModel(BackendModelGenerator, Generic[T]):
+    """Calliope's backend model functionality."""
+
     def __init__(self, inputs: xr.Dataset, instance: T, **kwargs) -> None:
         """Abstract base class to build backend models that interface with solvers.
 
         Args:
             inputs (xr.Dataset): Calliope model data.
             instance (T): Interface model instance.
+            **kwargs: build configuration overrides.
         """
         super().__init__(inputs, **kwargs)
         self._instance = instance
@@ -580,8 +574,7 @@ def __init__(self, inputs: xr.Dataset, instance: T, **kwargs) -> None:
 
     @abstractmethod
     def get_parameter(self, name: str, as_backend_objs: bool = True) -> xr.DataArray:
-        """
-        Extract parameter from backend dataset.
+        """Extract parameter from backend dataset.
 
         Args:
             name (str): Name of parameter.
@@ -599,9 +592,9 @@ def get_parameter(self, name: str, as_backend_objs: bool = True) -> xr.DataArray
     def get_constraint(
         self, name: str, as_backend_objs: bool = True, eval_body: bool = False
     ) -> Union[xr.DataArray, xr.Dataset]:
-        """
-        Get constraint data as either a table of details or as an array of backend interface objects.
-        Can be used to inspect and debug built constraints.
+        """Get constraint data from the backend for debugging.
+
+        Dat can be returned as  a table of details or as an array of backend interface objects
 
         Args:
             name (str): Name of constraint, as given in YAML constraint key.
@@ -625,7 +618,7 @@ def get_constraint(
 
     @abstractmethod
     def get_variable(self, name: str, as_backend_objs: bool = True) -> xr.DataArray:
-        """Extract decision variable array from backend dataset
+        """Extract decision variable array from backend dataset.
 
         Args:
             name (str): Name of variable.
@@ -642,7 +635,7 @@ def get_variable(self, name: str, as_backend_objs: bool = True) -> xr.DataArray:
 
     @abstractmethod
     def get_variable_bounds(self, name: str) -> xr.Dataset:
-        """Extract decision variable upper and lower bound array from backend dataset
+        """Extract decision variable upper and lower bound array from backend dataset.
 
         Args:
             name (str): Name of variable.
@@ -655,7 +648,7 @@ def get_variable_bounds(self, name: str) -> xr.Dataset:
     def get_global_expression(
         self, name: str, as_backend_objs: bool = True, eval_body: bool = True
     ) -> xr.DataArray:
-        """Extract global expression array from backend dataset
+        """Extract global expression array from backend dataset.
 
         Args:
             name (str): Name of global expression
@@ -681,12 +674,18 @@ def update_parameter(
         self, name: str, new_values: Union[xr.DataArray, SupportsFloat]
     ) -> None:
         """Update parameter elements using an array of new values.
-        If the parameter has not been previously defined, it will be added to the optimisation problem based on the new values given (with NaNs reverting to default values).
-        If the new values have fewer dimensions than are on the parameter array, the new values will be broadcast across the missing dimensions before applying the update.
+
+        If the parameter has not been previously defined, it will be added to the
+        optimisation problem based on the new values given (with NaNs reverting to
+        default values).
+        If the new values have fewer dimensions than are on the parameter array, the
+        new values will be broadcast across the missing dimensions before applying the
+        update.
 
         Args:
             name (str): Parameter to update
-            new_values (Union[xr.DataArray, SupportsFloat]): New values to apply. Any empty (NaN) elements in the array will be skipped.
+            new_values (Union[xr.DataArray, SupportsFloat]): New values to apply. Any
+                empty (NaN) elements in the array will be skipped.
         """
 
     @abstractmethod
@@ -697,8 +696,8 @@ def update_variable_bounds(
         min: Optional[Union[xr.DataArray, SupportsFloat]] = None,
         max: Optional[Union[xr.DataArray, SupportsFloat]] = None,
     ) -> None:
-        """
-        Update the bounds on a decision variable.
+        """Update the bounds on a decision variable.
+
         If the variable bounds are defined by parameters in the math formulation,
         the parameters themselves will be updated.
 
@@ -714,8 +713,8 @@ def update_variable_bounds(
 
     @abstractmethod
     def fix_variable(self, name: str, where: Optional[xr.DataArray] = None) -> None:
-        """
-        Fix the variable value to the value quantified on the most recent call to `solve`.
+        """Fix the variable value to the value quantified on the most recent call to `solve`.
+
         Fixed variables will be treated as parameters in the optimisation.
 
         Args:
@@ -728,8 +727,7 @@ def fix_variable(self, name: str, where: Optional[xr.DataArray] = None) -> None:
 
     @abstractmethod
     def unfix_variable(self, name: str, where: Optional[xr.DataArray] = None) -> None:
-        """
-        Unfix the variable so that it is treated as a decision variable in the next call to `solve`.
+        """Unfix the variable so that it is treated as a decision variable in the next call to `solve`.
 
         Args:
             name (str): Variable to update
@@ -741,19 +739,23 @@ def unfix_variable(self, name: str, where: Optional[xr.DataArray] = None) -> Non
 
     @abstractmethod
     def verbose_strings(self) -> None:
-        """
-        Update optimisation model object string representations to include the index coordinates of the object.
+        """Update optimisation model object string representations to include the index coordinates of the object.
 
         E.g., `variables(flow_out)[0]` will become `variables(flow_out)[power, region1, ccgt, 2005-01-01 00:00]`
 
-        This takes approximately 10% of the peak memory required to initially build the optimisation problem, so should only be invoked if inspecting the model in detail (e.g., debugging)
+        This takes approximately 10% of the peak memory required to initially build the
+        optimisation problem, so should only be invoked if inspecting the model in
+        detail (e.g., debugging).
 
-        Only string representations of model parameters and variables will be updated since global expressions automatically show the string representation of their contents.
+        Only string representations of model parameters and variables will be updated
+        since global expressions automatically show the string representation of their
+        contents.
         """
 
     @abstractmethod
     def to_lp(self, path: Union[str, Path]) -> None:
         """Write the optimisation problem to file in the linear programming LP format.
+
         The LP file can be used for debugging and to submit to solvers directly.
 
         Args:
@@ -763,57 +765,56 @@ def to_lp(self, path: Union[str, Path]) -> None:
     @property
     @abstractmethod
     def has_integer_or_binary_variables(self) -> bool:
-        """Confirm whether or not the built model has binary or integer decision variables.
+        """Confirms if the built model has binary or integer decision variables.
 
-        This can be used to understand how long the optimisation may take (MILP problems are harder to solve than LP ones),
-        and to verify whether shadow prices can be tracked (they cannot be tracked in MILP problems).
+        This can be used to understand how long the optimisation may take (MILP
+        problems are harder to solve than LP ones), and to verify whether shadow prices
+        can be tracked (they cannot be tracked in MILP problems).
 
         Returns:
-            bool: If True, the built model has binary or integer decision variables. If False, all decision variables are continuous.
+            bool: True if the built model has binary or integer decision variables.
+                False if all decision variables are continuous.
         """
 
     @abstractmethod
     def _solve(
         self,
         solver: str,
-        solver_io: Optional[str] = None,
-        solver_options: Optional[dict] = None,
-        save_logs: Optional[str] = None,
+        solver_io: str | None = None,
+        solver_options: dict | None = None,
+        save_logs: str | None = None,
         warmstart: bool = False,
         **solve_config,
     ) -> xr.Dataset:
-        """
-        Optimise built model. If solution is optimal, interface objects
-        (decision variables, global expressions, constraints, objective) can be successfully
-        evaluated for their values at optimality.
+        """Optimise built model.
+
+        If solution is optimal, interface objects (decision variables, global
+        expressions, constraints, objective) can be successfully evaluated for their
+        values at optimality.
 
         Args:
             solver (str): Name of solver to optimise with.
-            solver_io (Optional[str], optional):
-                If chosen solver has a python interface, set to "python" for potential
+            solver_io (str | None, optional): If chosen solver has a python interface, set to "python" for potential
                 performance gains, otherwise should be left as None. Defaults to None.
-            solver_options (Optional[dict], optional):
-                Solver options/parameters to pass directly to solver.
-                See solver documentation for available parameters that can be influenced.
-                Defaults to None.
-            save_logs (Optional[str], optional):
-                If given, solver logs and built LP file will be saved to this filepath.
+            solver_options (dict | None, optional): Solver options/parameters to pass directly to solver.
+                See solver documentation for available parameters that can be influenced. Defaults to None.
+            save_logs (str | None, optional): If given, solver logs and built LP file will be saved to this filepath.
                 Defaults to None.
-            warmstart (bool, optional):
-                If True, and the chosen solver is capable of implementing it, an existing
+            warmstart (bool, optional): If True, and the chosen solver is capable of implementing it, an existing
                 optimal solution will be used to warmstart the next solve run.
                 Defaults to False.
+            **solve_config: solve configuration overrides.
 
         Returns:
-            xr.Dataset:
-                Dataset of decision variable values if the solution was optimal/feasible,
+            xr.Dataset: Dataset of decision variable values if the solution was optimal/feasible,
                 otherwise an empty dataset.
         """
 
     def load_results(self) -> xr.Dataset:
-        """
-        Evaluate backend decision variables, global expressions, parameters (if not in
-        inputs), and shadow_prices (if tracked), after a successful model run.
+        """Load and evaluate model results after a successful run.
+
+        Evaluates backend decision variables, global expressions, parameters (if not in
+        inputs), and shadow_prices (if tracked).
 
         Returns:
             xr.Dataset: Dataset of optimal solution results (all numeric data).
@@ -850,8 +851,7 @@ def _drop_attrs(da):
         return results
 
     def _find_all_references(self, initial_references: set) -> set:
-        """
-        Find all nested references to optimisation problem components from an initial set of references.
+        """Find all nested references to optimisation problem components from an initial set of references.
 
         Args:
             initial_references (set): names of optimisation problem components.
@@ -859,7 +859,6 @@ def _find_all_references(self, initial_references: set) -> set:
         Returns:
             set: `initial_references` + any names of optimisation problem components referenced from those initial references.
         """
-
         references = initial_references.copy()
         for reference in initial_references:
             new_refs = self._dataset[reference].attrs.get("references", {})
@@ -911,25 +910,25 @@ def get(self, name) -> xr.DataArray:
 
     @abstractmethod
     def activate(self):
-        "Activate shadow price tracking."
+        """Activate shadow price tracking."""
 
     @abstractmethod
     def deactivate(self):
-        "Deactivate shadow price tracking."
+        """Deactivate shadow price tracking."""
 
     @property
     @abstractmethod
     def is_active(self) -> bool:
-        "Check whether shadow price tracking is active or not"
+        """Check whether shadow price tracking is active or not."""
 
     @property
     @abstractmethod
     def available_constraints(self) -> Iterable:
-        "Iterable of constraints that are available to provide shadow prices on"
+        """Iterable of constraints that are available to provide shadow prices on."""
 
     @property
     def tracked(self) -> set:
-        "Constraints being tracked for automatic addition to the results dataset"
+        """Constraints being tracked for automatic addition to the results dataset."""
         return self._tracked
 
     def track_constraints(self, constraints_to_track: list):
diff --git a/src/calliope/backend/expression_parser.py b/src/calliope/backend/expression_parser.py
index b53316c1..01497a38 100644
--- a/src/calliope/backend/expression_parser.py
+++ b/src/calliope/backend/expression_parser.py
@@ -27,6 +27,7 @@
 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 ##
+"""Expression parsing functionality."""
 
 from __future__ import annotations
 
@@ -39,7 +40,6 @@
     Iterable,
     Iterator,
     Literal,
-    Optional,
     Union,
     overload,
 )
@@ -62,6 +62,8 @@
 
 
 class EvalAttrs(TypedDict):
+    """Attribute checker class."""
+
     equation_name: str
     where_array: xr.DataArray
     slice_dict: dict
@@ -77,36 +79,40 @@ class EvalAttrs(TypedDict):
 
 
 class EvalString(ABC):
-    "Parent class for all string evaluation classes - used in type hinting"
+    """Parent class for all string evaluation classes - used in type hinting."""
+
     name: str
     eval_attrs: EvalAttrs
 
     def __eq__(self, other):
+        """Functionality for '==' operations."""
         return self.__repr__() == other
 
     @abstractmethod
     def __repr__(self) -> str:
-        "Return string representation of the parsed grammar"
+        """Return string representation of the parsed grammar."""
 
 
 class EvalToArrayStr(EvalString):
+    """Evaluation for string arrays."""
+
     @abstractmethod
     def as_math_string(self) -> str:
-        """Return evaluated expression as a LaTex math string"""
+        """Evaluate and return expression as LaTeX."""
 
     @abstractmethod
     def as_array(self) -> xr.DataArray | list[str | float]:
-        """Return evaluated expression as an array"""
+        """Evaluate and return expression as a DataArray or list."""
 
+    # Math strings evaluate to strings.
     @overload
-    def eval(self, return_type: Literal["math_string"], **eval_kwargs) -> str:
-        """math strings evaluate to strings"""
+    def eval(self, return_type: Literal["math_string"], **eval_kwargs) -> str: ...
 
+    # Arrays evaluate to arrays
     @overload
     def eval(
         self, return_type: Literal["array"], **eval_kwargs
-    ) -> xr.DataArray | list[str | float]:
-        """arrays evaluate to arrays"""
+    ) -> xr.DataArray | list[str | float]: ...
 
     def eval(
         self, return_type: RETURN_T, **eval_kwargs
@@ -116,6 +122,8 @@ def eval(
         Args:
             return_type (Literal[math_string, input, array]):
                 Dictates how the expression should be evaluated (see `Returns` section).
+            **eval_kwargs: arbitrary keyword arguments.
+
         Keyword Args:
             equation_name (str): Name of math component in which expression is defined.
             slice_dict (dict): Dictionary mapping the index slice name to a parsed equation expression.
@@ -126,12 +134,12 @@ def eval(
             references (set): any references in the math string to other model components.
             helper_functions (dict[str, type[ParsingHelperFunction]]): Dictionary of allowed helper functions.
             as_values (bool, optional): Return array as numeric values, not backend objects. Defaults to False.
+
         Returns:
             Union[str, list[str | float], xr.DataArray]:
                 If `math_string` is desired, returns a valid LaTex math string.
                 If `array` is desired, returns xarray DataArray or a list of strings/numbers (if the expression represents a list).
         """
-
         self.eval_attrs = eval_kwargs
         evaluated: Union[str, list[str | float], xr.DataArray]
         if return_type == "array":
@@ -142,18 +150,24 @@ def eval(
 
 
 class EvalToCallable(EvalString):
+    """Parent class for callable functionality."""
+
     @abstractmethod
     def as_callable(self, return_type: RETURN_T) -> Callable:
-        """"""
+        """Callable processing."""
+        ...
 
     def eval(
-        self, return_type: RETURN_T, **eval_kwargs: Unpack[EvalAttrs]  # type: ignore
+        self,
+        return_type: RETURN_T,
+        **eval_kwargs: Unpack[EvalAttrs],  # type: ignore
     ) -> Callable:
         """Evaluate math string expression.
-        Args and kwargs are passed on directly to helper function.
 
         Args:
             return_type (str): Whether to return a math string or xarray DataArray.
+            **eval_kwargs: passed on directly to helper function..
+
         Keyword Args:
             equation_name (str): Name of math component in which expression is defined.
             slice_dict (dict): Dictionary mapping the index slice name to a parsed equation expression.
@@ -164,16 +178,18 @@ def eval(
             references (set): any references in the math string to other model components.
             helper_functions (dict[str, type[ParsingHelperFunction]]): Dictionary of allowed helper functions.
             as_values (bool, optional): Return array as numeric values, not backend objects. Defaults to False.
+
         Returns:
             Callable: returns helper function.
         """
-
         self.eval_attrs = eval_kwargs
         evaluated = self.as_callable(return_type)
         return evaluated
 
 
 class EvalOperatorOperand(EvalToArrayStr):
+    """Evaluation of math operands."""
+
     LATEX_OPERATOR_LOOKUP: dict[str, str] = {
         "**": "{val}^{{{operand}}}",
         "*": r"{val} \times {operand}",
@@ -184,9 +200,9 @@ class EvalOperatorOperand(EvalToArrayStr):
     SKIP_IF: list[str] = ["+", "-"]
 
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed expressions with operands separated
-        by an operator (OPERAND OPERATOR OPERAND OPERATOR OPERAND ...)
+        """Process successfully parsed expressions with operands separated by an operator.
+
+        I.e.: OPERAND OPERATOR OPERAND OPERATOR OPERAND ...
 
         Args:
             tokens (pp.ParseResults):
@@ -197,6 +213,7 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         first_operand = self.value[0].__repr__()
         operand_operator_pairs = " ".join(
             op + " " + val.__repr__()
@@ -208,7 +225,7 @@ def __repr__(self) -> str:
     def _operator_operands(
         self, token_list: list
     ) -> Iterator[tuple[str, pp.ParseResults]]:
-        "Generator to extract operators and operands in pairs"
+        """Generator to extract operators and operands in pairs."""
         it = iter(token_list)
         while 1:
             try:
@@ -217,7 +234,7 @@ def _operator_operands(
                 break
 
     def _apply_where_array(self, evaluated: xr.DataArray) -> xr.DataArray:
-        "eval util function to apply where arrays to non-latex strings"
+        """Util function to apply where arrays to non-latex strings."""
         where_array = self.eval_attrs["where_array"]
         try:
             evaluated = evaluated.where(where_array)
@@ -237,7 +254,7 @@ def _skip_component_on_conditional(self, component: str, operator_: str) -> bool
     def _operate(
         val: xr.DataArray, evaluated_operand: xr.DataArray, operator_: str
     ) -> xr.DataArray:
-        "Apply evaluated operation on two dataarrays"
+        """Apply evaluated operation on two DataArrays."""
         match operator_:
             case "**":
                 val = val**evaluated_operand
@@ -251,7 +268,7 @@ def _operate(
                 val = val - evaluated_operand
         return val
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         val = self.value[0].eval(return_type="math_string", **self.eval_attrs)
 
         for operator_, operand in self._operator_operands(self.value[1:]):
@@ -273,7 +290,7 @@ def as_math_string(self) -> str:
                 )
         return val
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         val = self._apply_where_array(
             self.value[0].eval(return_type="array", **self.eval_attrs)
         )
@@ -287,9 +304,10 @@ def as_array(self) -> xr.DataArray:
 
 
 class EvalSignOp(EvalToArrayStr):
+    """Class for processing expressions with + or -."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed expressions with a leading + or - sign.
+        """Parse action to process successfully parsed expressions with a leading + or - sign.
 
         Args:
             tokens (pp.ParseResults):
@@ -299,24 +317,25 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return str(f"({self.sign}){self.value.__repr__()}")
 
+    # string return
     @overload
-    def _eval(self, return_type: Literal["math_string"]) -> str:
-        "string return"
+    def _eval(self, return_type: Literal["math_string"]) -> str: ...
 
+    # array return
     @overload
-    def _eval(self, return_type: Literal["array"]) -> xr.DataArray:
-        "array return"
+    def _eval(self, return_type: Literal["array"]) -> xr.DataArray: ...
 
     def _eval(self, return_type: RETURN_T) -> Union[xr.DataArray, str]:
-        "Evaluate the element that will have the sign attached to it"
+        """Evaluate the element that will have the sign attached to it."""
         return self.value.eval(return_type, **self.eval_attrs)
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102
         return self.sign + self._eval("math_string")
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         evaluated = self._eval("array")
         if self.sign == "-":
             evaluated = -1 * evaluated
@@ -324,11 +343,12 @@ def as_array(self) -> xr.DataArray:
 
 
 class EvalComparisonOp(EvalToArrayStr):
+    """Class for processing comparison operations."""
+
     OP_TRANSLATOR = {"<=": r" \leq ", ">=": r" \geq ", "==": " = "}
 
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed equations of the form LHS OPERATOR RHS.
+        """Parse action to process successfully parsed equations of the form LHS OPERATOR RHS.
 
         Args:
             tokens (pp.ParseResults):
@@ -338,27 +358,29 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return f"{self.lhs.__repr__()} {self.op} {self.rhs.__repr__()}"
 
+    # string return
     @overload
-    def _eval(self, return_type: Literal["math_string"]) -> tuple[str, str]:
-        "string return"
+    def _eval(self, return_type: Literal["math_string"]) -> tuple[str, str]: ...
 
+    # array return
     @overload
-    def _eval(self, return_type: Literal["array"]) -> tuple[xr.DataArray, xr.DataArray]:
-        "array return"
+    def _eval(
+        self, return_type: Literal["array"]
+    ) -> tuple[xr.DataArray, xr.DataArray]: ...
 
     def _eval(
         self, return_type: RETURN_T
     ) -> Union[tuple[str, str], tuple[xr.DataArray, xr.DataArray]]:
-        "Evaluate the LHS and RHS of the comparison"
+        """Evaluate the LHS and RHS of the comparison."""
         lhs = self.lhs.eval(return_type, **self.eval_attrs)
         rhs = self.rhs.eval(return_type, **self.eval_attrs)
         return lhs, rhs
 
     def _compare_bitwise(self, where: bool, lhs: Any, rhs: Any) -> Any:
-        "Comparison function for application to individual elements of the array"
-
+        """Comparison function for application to individual elements of the array."""
         if not where or pd.isnull(lhs) or pd.isnull(rhs):
             return np.nan
         match self.op:
@@ -370,11 +392,11 @@ def _compare_bitwise(self, where: bool, lhs: Any, rhs: Any) -> Any:
                 constraint = lhs >= rhs
         return constraint
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         lhs, rhs = self._eval("math_string")
         return lhs + self.OP_TRANSLATOR[self.op] + rhs
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         lhs, rhs = self._eval("array")
         return self.eval_attrs["backend_interface"]._apply_func(
             self._compare_bitwise, self.eval_attrs["where_array"], lhs, rhs
@@ -382,10 +404,12 @@ def as_array(self) -> xr.DataArray:
 
 
 class EvalFunction(EvalToArrayStr):
+    """Class to process parsed functions."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed helper function strings of the form
-        helper_function_name(*args, **eval_kwargs).
+        """Parse action to process successfully parsed helper function strings.
+
+        Strings must be in the following form: helper_function_name(*args, **eval_kwargs).
 
         Args:
             tokens (pp.ParseResults):
@@ -399,23 +423,22 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         _kwargs = ", ".join(f"{k}={v}" for k, v in self.kwargs.items())
         return f"{str(self.func_name)}(args={self.args}, kwargs={{{_kwargs}}})"
 
     @overload
-    def _arg_eval(self, return_type: Literal["math_string"], arg: Any) -> str:
-        "string return"
+    def _arg_eval(self, return_type: Literal["math_string"], arg: Any) -> str: ...
 
     @overload
     def _arg_eval(
         self, return_type: Literal["array"], arg: Any
-    ) -> xr.DataArray | list[str | float]:
-        "array return"
+    ) -> xr.DataArray | list[str | float]: ...
 
     def _arg_eval(
         self, return_type: RETURN_T, arg: Any
     ) -> Union[str, xr.DataArray, list[str | float]]:
-        "Evaluate the arguments of the helper function"
+        """Evaluate the arguments of the helper function."""
         if isinstance(arg, pp.ParseResults):
             evaluated = arg[0].eval(return_type, **self.eval_attrs)
         elif isinstance(arg, list):
@@ -427,16 +450,13 @@ def _arg_eval(
         return evaluated
 
     @overload
-    def _eval(self, return_type: Literal["math_string"]) -> str:
-        "string return"
+    def _eval(self, return_type: Literal["math_string"]) -> str: ...
 
     @overload
-    def _eval(self, return_type: Literal["array"]) -> xr.DataArray:
-        "array return"
+    def _eval(self, return_type: Literal["array"]) -> xr.DataArray: ...
 
     def _eval(self, return_type: RETURN_T) -> Union[str, xr.DataArray]:
-        "Pass evaluated arguments to evaluated helper function"
-
+        """Pass evaluated arguments to evaluated helper function."""
         helper_function = self.func_name.eval(return_type, **self.eval_attrs)
         if helper_function.ignore_where:
             self.eval_attrs["where_array"] = xr.DataArray(True)
@@ -452,17 +472,19 @@ def _eval(self, return_type: RETURN_T) -> Union[str, xr.DataArray]:
         evaluated = helper_function(*args_, **kwargs_)
         return evaluated
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         return self._eval("math_string")
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         return self._eval("array")
 
 
 class EvalHelperFuncName(EvalToCallable):
+    """For processing parsed helper function names."""
+
     def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed helper function names.
+        """Parse action to process successfully parsed helper function names.
+
         This is a unique parse action so that we can catch invalid helper functions
         most safely.
 
@@ -474,15 +496,17 @@ def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
             tokens (pp.ParseResults):
                 Has one parsed element: helper_function_name (str).
         """
-        self.name = self.value = tokens[0]  # type: str
+        self.name = self.value = tokens[0]
         self.instring = instring
         self.loc = loc
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return str(self.name)
 
     def as_callable(self, return_type: RETURN_T) -> Callable:
+        """Evalluate and return the callable action of the helper function."""
         helper_functions = self.eval_attrs["helper_functions"]
         equation_name = self.eval_attrs["equation_name"]
         if self.name not in helper_functions.keys():
@@ -499,10 +523,12 @@ def as_callable(self, return_type: RETURN_T) -> Callable:
 
 
 class EvalSlicedComponent(EvalToArrayStr):
+    """For processing of sliced parameters / decision variables."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed sliced parameters or decision variables
-        of the form param_or_var[*slices].
+        """Process successfully parsed sliced parameters or decision variables.
+
+        In the form of param_or_var[*slices].
 
         Args:
             tokens (pp.ParseResults):
@@ -518,6 +544,7 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         slices = ", ".join(f"{k}={v.__repr__()}" for k, v in self.slices.items())
         return f"SLICED_{self.obj_name}[{slices}]"
 
@@ -543,15 +570,13 @@ def __replace(term):
         return __replace
 
     @overload
-    def _eval(self, return_type: Literal["math_string"]) -> tuple[str, dict]:
-        "string return"
+    def _eval(self, return_type: Literal["math_string"]) -> tuple[str, dict]: ...
 
     @overload
-    def _eval(self, return_type: Literal["array"]) -> tuple[xr.DataArray, dict]:
-        "array return"
+    def _eval(self, return_type: Literal["array"]) -> tuple[xr.DataArray, dict]: ...
 
     def _eval(self, return_type: RETURN_T) -> tuple[Union[str, xr.DataArray], dict]:
-        "Evaluate the slice dim and vals of each slice element"
+        """Evaluate the slice dim and vals of each slice element."""
         slices: dict[str, Any] = {
             k: v.eval(return_type, **self.eval_attrs) for k, v in self.slices.items()
         }
@@ -559,7 +584,7 @@ def _eval(self, return_type: RETURN_T) -> tuple[Union[str, xr.DataArray], dict]:
         evaluated = self.obj_name.eval(return_type, **self.eval_attrs)
         return evaluated, slices
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         evaluated, slices = self._eval("math_string")
         singular_slice_refs = {k.removesuffix("s"): v for k, v in slices.items()}
         id_ = pp.Combine(
@@ -574,16 +599,16 @@ def as_math_string(self) -> str:
         obj_parser.set_parse_action(self._replace_rule(singular_slice_refs))
         return obj_parser.parse_string(evaluated, parse_all=True)[0]
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         evaluated, slices = self._eval("array")
         return evaluated.sel(**slices)
 
 
 class EvalIndexSlice(EvalToArrayStr):
+    """For processing `$slice` expressions."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed expression index slice references
-        of the form `$slice`.
+        """Process successfully parsed expression index `$slice` references.
 
         Args:
             tokens (pp.ParseResults):
@@ -593,31 +618,30 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return "REFERENCE:" + str(self.name)
 
     @overload
-    def _eval(self, return_type: Literal["math_string"], as_values: bool) -> str:
-        "string return"
+    def _eval(self, return_type: Literal["math_string"], as_values: bool) -> str: ...
 
     @overload
     def _eval(
         self, return_type: Literal["array"], as_values: bool
-    ) -> xr.DataArray | list[str | float]:
-        "array return"
+    ) -> xr.DataArray | list[str | float]: ...
 
     def _eval(
         self, return_type: RETURN_T, as_values: bool
     ) -> Union[str, xr.DataArray, list[str | float]]:
-        "Evaluate the referenced `slice`."
+        """Evaluate the referenced `slice`."""
         self.eval_attrs["as_values"] = as_values
         return self.eval_attrs["slice_dict"][self.name][0].eval(
             return_type, **self.eval_attrs
         )
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         return self._eval("math_string", False)
 
-    def as_array(self) -> xr.DataArray | list[str | float]:
+    def as_array(self) -> xr.DataArray | list[str | float]:  # noqa: D102, override
         evaluated = self._eval("array", True)
         if isinstance(evaluated, xr.DataArray) and evaluated.isnull().any():
             evaluated = evaluated.notnull()
@@ -625,10 +649,10 @@ def as_array(self) -> xr.DataArray | list[str | float]:
 
 
 class EvalSubExpressions(EvalToArrayStr):
+    """For processing sub-expressions."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed sub-expressions of the form
-        `$sub_expressions`.
+        """Process successfully parsed `$sub_expressions`.
 
         Args:
             tokens (pp.ParseResults):
@@ -638,34 +662,36 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return "SUB_EXPRESSION:" + str(self.name)
 
     @overload
-    def _eval(self, return_type: Literal["math_string"]) -> str:
-        "string return"
+    def _eval(self, return_type: Literal["math_string"]) -> str: ...
 
     @overload
-    def _eval(self, return_type: Literal["array"]) -> xr.DataArray:
-        "array return"
+    def _eval(self, return_type: Literal["array"]) -> xr.DataArray: ...
 
     def _eval(self, return_type: RETURN_T) -> Union[str, xr.DataArray]:
-        "Evaluate the referenced sub_expression"
+        """Evaluate the referenced sub_expression."""
         return self.eval_attrs["sub_expression_dict"][self.name][0].eval(
             return_type, **self.eval_attrs
         )
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         return self._eval("math_string")
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         return self._eval("array")
 
 
 class EvalNumber(EvalToArrayStr):
+    """For processing numbers."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed numbers described as integers (1),
-        floats (1.), or in scientific notation (1e1). Also capture infinity (inf/.inf).
+        """Process successfully parsed numbers.
+
+        Catches integers (1), floats (1.), and in scientific notation (1e1).
+        Also capture infinity (inf/.inf).
 
         Args:
             tokens (pp.ParseResults):
@@ -675,23 +701,26 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return "NUM:" + str(self.value)
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         return re.sub(
             r"([\d]+?)e([+-])([\d]+)",
             r"\1\\mathord{\\times}10^{\2\3}",
             f"{float(self.value):.6g}",
         )
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         return xr.DataArray(float(self.value), attrs={"obj_type": "number"})
 
 
 class ListParser(EvalToArrayStr):
+    """For parsing lists."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed lists of generic strings.
+        """Process successfully parsed lists of generic strings.
+
         This is required since we call "eval()" on all elements of the where string,
         so lists of strings need to be evaluatable as a whole "package".
 
@@ -701,13 +730,14 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.val = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return f"{self.val}"
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         input_list = self.as_array()
         return "[" + ",".join(str(i) for i in input_list) + "]"
 
-    def as_array(self) -> list[str | float]:
+    def as_array(self) -> list[str | float]:  # noqa: D102, override
         values = [val.eval("array", **self.eval_attrs) for val in self.val]
         # strings and numbers are returned as xarray arrays of size 1,
         # so we extract those values.
@@ -715,9 +745,11 @@ def as_array(self) -> list[str | float]:
 
 
 class EvalUnslicedComponent(EvalToArrayStr):
+    """Evaluation of unsliced components."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed generic strings.
+        """Parse action to process successfully parsed generic strings.
+
         This is required since we call "eval()" on all elements of the where string,
         so even arbitrary strings (used in comparison operations) need to be evaluatable.
 
@@ -729,9 +761,10 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.values = tokens
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return f"COMPONENT:{self.name}"
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         self.eval_attrs["as_values"] = False
         evaluated = self.as_array()
         self.eval_attrs["references"].add(self.name)
@@ -746,7 +779,7 @@ def as_math_string(self) -> str:
             formatted_name = rf"\textit{{{self.name}}}"
         return formatted_name + dims
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         backend_interface = self.eval_attrs["backend_interface"]
 
         if self.eval_attrs.get("as_values", False):
@@ -766,9 +799,11 @@ def as_array(self) -> xr.DataArray:
 
 
 class EvalGenericString(EvalToArrayStr):
+    """For generic string parsing."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed generic strings.
+        """Process successfully parsed generic strings.
+
         This is required since we call "eval()" on all elements of the where string,
         so even arbitrary strings (used in comparison operations) need to be evaluatable.
 
@@ -778,12 +813,13 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.val = tokens[0]
 
     def __repr__(self) -> str:
+        """Programming / official string representation."""
         return f"STRING:{self.val}"
 
-    def as_math_string(self):
+    def as_math_string(self):  # noqa: D102, override
         return str(self.val)
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         return xr.DataArray(str(self.val), attrs={"obj_type": "string"})
 
 
@@ -792,8 +828,7 @@ def helper_function_parser(
     generic_identifier: pp.ParserElement,
     allow_function_in_function: bool = False,
 ) -> pp.ParserElement:
-    """
-    Parsing grammar to process helper functions of the form `helper_function(*args, **eval_kwargs)`.
+    """Process helper functions of the form `helper_function(*args, **eval_kwargs)`.
 
     Helper functions can accept other parser elements as arguments,
     i.e., components, parameters or variables, numbers, and other functions.
@@ -823,7 +858,6 @@ def helper_function_parser(
             Parser for functions which will call the function with the specified
             arguments on evaluation.
     """
-
     helper_function = pp.Forward()
     allowed_parser_elements_in_args = list(args)
     lpar = pp.Suppress("(")
@@ -866,9 +900,9 @@ def sliced_param_or_var_parser(
     unsliced_object: pp.ParserElement,
     allow_slice_references: bool = True,
 ) -> pp.ParserElement:
-    """
-    Parsing grammar to process strings representing sliced model parameters or variables,
-    e.g. "source_use_max[node, tech]".
+    """Process strings representing sliced model parameters or variables.
+
+    E.g. "source_use_max[node, tech]".
 
     If a parameter, must be a data variable in the Model._model_data xarray dataset.
 
@@ -898,7 +932,6 @@ def sliced_param_or_var_parser(
             Parser which returns a dictionary with name of parameter/variable and list
             of index items as separate entries on.
     """
-
     lspar = pp.Suppress("[")
     rspar = pp.Suppress("]")
 
@@ -922,8 +955,9 @@ def sliced_param_or_var_parser(
 
 
 def sub_expression_parser(generic_identifier: pp.ParserElement) -> pp.ParserElement:
-    """
-    Parse strings prepended with the YAML constraint sub-expression classifier `$`. E.g. "$my_sub_expr"
+    """Parse strings prepended with the YAML constraint sub-expression classifier `$`.
+
+    E.g. "$my_sub_expr"
 
     Args:
         generic_identifier (pp.ParserElement):
@@ -934,7 +968,6 @@ def sub_expression_parser(generic_identifier: pp.ParserElement) -> pp.ParserElem
         pp.ParserElement:
             Parser which produces a dictionary of the form {"sub_expression": "my_sub_expression"} on evaluation.
     """
-
     sub_expression = pp.Combine(
         pp.Suppress(SUB_EXPRESSION_CLASSIFIER) + generic_identifier
     )
@@ -944,8 +977,9 @@ def sub_expression_parser(generic_identifier: pp.ParserElement) -> pp.ParserElem
 
 
 def unsliced_object_parser(valid_component_names: Iterable[str]) -> pp.ParserElement:
-    """
-    Create a copy of the generic identifier and set a parse action to find the string in
+    """Parse unsliced objects and identify their corresponding parse actions.
+
+    Creates a copy of the generic identifier and sets a parse action to find the string in
     the list of input parameters or optimisation decision variables.
 
     Args:
@@ -957,7 +991,6 @@ def unsliced_object_parser(valid_component_names: Iterable[str]) -> pp.ParserEle
             Copy of input parser with added parse action to lookup an unsliced
             parameter/variable value
     """
-
     unsliced_param_or_var = pp.one_of(valid_component_names, as_keyword=True)
     unsliced_param_or_var.set_parse_action(EvalUnslicedComponent)
 
@@ -967,8 +1000,7 @@ def unsliced_object_parser(valid_component_names: Iterable[str]) -> pp.ParserEle
 def evaluatable_identifier_parser(
     identifier: pp.ParserElement, valid_component_names: Iterable
 ) -> pp.ParserElement:
-    """
-    Create an evaluatable copy of the generic identifier that will return a string or a model component as an array.
+    """Create an evaluatable copy of the generic identifier that will return a string or a model component as an array.
 
     Args:
         identifier (pp.ParserElement):
@@ -992,8 +1024,7 @@ def evaluatable_identifier_parser(
 def list_parser(
     number: pp.ParserElement, evaluatable_identifier: pp.ParserElement
 ) -> pp.ParserElement:
-    """
-    Parse strings which define a list of other strings or numbers.
+    """Parse strings which define a list of other strings or numbers.
 
     Lists are defined as anything wrapped in square brackets (`[]`).
 
@@ -1013,17 +1044,14 @@ def list_parser(
 
 
 def setup_base_parser_elements() -> tuple[pp.ParserElement, pp.ParserElement]:
-    """
-    Setup parser elements that will be components of other parsers.
+    """Setup parser elements that will be components of other parsers.
 
     Returns:
-        number (pp.ParserElement):
-            Parser for numbers (integer, float, scientific notation, "inf"/".inf").
-        generic_identifier (pp.ParserElement):
-            Parser for valid python variables without leading underscore and not called "inf".
-            This parser has no parse action.
+        tuple[pp.ParserElement, pp.ParserElement]: (number, generic_identifier)
+            number: parser for numbers (integer, float, scentific notation, "inf"/".inf").
+            generic_identifier: parser for valid python variables without leading
+                underscore and not called "inf". This parser has no parse action.
     """
-
     inf_kw = pp.Combine(pp.Opt(pp.Suppress(".")) + pp.Keyword("inf", caseless=True))
     number = pp.pyparsing_common.number | inf_kw
     generic_identifier = ~inf_kw + pp.Word(pp.alphas, pp.alphanums + "_")
@@ -1033,31 +1061,26 @@ def setup_base_parser_elements() -> tuple[pp.ParserElement, pp.ParserElement]:
     return number, generic_identifier
 
 
-def arithmetic_parser(*args, arithmetic: Optional[pp.Forward] = None) -> pp.Forward:
-    """
-    Parsing grammar to combine equation elements using basic arithmetic (+, -, *, /, **).
-    Can handle the difference between a sign (e.g., -1,+1) and a addition/subtraction (0 - 1, 0 + 1).
+def arithmetic_parser(*args, arithmetic: pp.Forward | None = None) -> pp.Forward:
+    """Parsing grammar to combine equation elements using basic arithmetic (+, -, *, /, **).
 
+    Can handle the difference between a sign (e.g., -1,+1) and a addition/subtraction (0 - 1, 0 + 1).
     Whitespace is ignored on parsing (i.e., "1+1+foo" is equivalent to "1 + 1 + foo").
 
     Args:
-        helper_function (pp.ParserElement):
-            Parsing grammar to process helper functions of the form `helper_function(*args, **eval_kwargs)`.
-        sliced_param_or_var (pp.ParserElement):
-            Parser for sliced parameters or variables, e.g. "foo[bar]"
-        sub_expression (pp.ParserElement):
-            Parser for constraint sub expressions, e.g. "$foo"
-        unsliced_param_or_var (pp.ParserElement):
-            Parser for unsliced parameters or variables, e.g. "foo"
-        number (pp.ParserElement):
-            Parser for numbers (integer, float, scientific notation, "inf"/".inf").
-    Kwargs:
-        arithmetic (Optional[pp.Forward]):
-            If given, will add arithmetic rules to this existing parsing rule.
-            Defaults to None (i.e., arithmetic rules will be a newly generated rule).
+        *args: arguments in the form of a list. These can be:
+            helper_function (pp.ParserElement): parsing grammar to process helper functions
+                of the form `helper_function(*args, **eval_kwargs)`.
+            sliced_param_or_var (pp.ParserElement): parser for sliced parameters or variables, e.g. "foo[bar]"
+            sub_expression (pp.ParserElement): parser for constraint sub expressions, e.g. "$foo"
+            unsliced_param_or_var (pp.ParserElement): parser for unsliced parameters or variables, e.g. "foo"
+            number (pp.ParserElement): parser for numbers (integer, float, scientific notation, "inf"/".inf").
+        arithmetic (pp.Forward | None, optional): If given, add arithmetic rules to this
+            existing parsing rule (otherwise, arithmetic rules will be a newly generated rule).
+            Defaults to None.
+
     Returns:
-        pp.ParserElement:
-            Parser for strings which use arithmetic operations to combine other parser elements.
+        pp.Forward: parser for strings which use arithmetic operations to combine other parser elements.
     """
     signop = pp.one_of(["+", "-"])
     multop = pp.one_of(["*", "/"])
@@ -1080,9 +1103,7 @@ def arithmetic_parser(*args, arithmetic: Optional[pp.Forward] = None) -> pp.Forw
 
 
 def equation_comparison_parser(arithmetic: pp.ParserElement) -> pp.ParserElement:
-    """
-    Parsing grammar to combine equation elements either side of a comparison operator
-    (<= >= ==).
+    """Parsing grammar to combine equation elements either side of a comparison operator (<= >= ==).
 
     Whitespace is ignored on parsing (i.e., "1+foo==$bar" is equivalent to "1 + 1 == $bar").
 
@@ -1094,7 +1115,6 @@ def equation_comparison_parser(arithmetic: pp.ParserElement) -> pp.ParserElement
         pp.ParserElement:
             Parser for strings of the form "LHS OPERATOR RHS".
     """
-
     comparison_operators = pp.one_of(["<=", ">=", "=="])
     equation_comparison = arithmetic + comparison_operators + arithmetic
     equation_comparison.set_parse_action(EvalComparisonOp)
@@ -1103,9 +1123,9 @@ def equation_comparison_parser(arithmetic: pp.ParserElement) -> pp.ParserElement
 
 
 def generate_slice_parser(valid_component_names: Iterable) -> pp.ParserElement:
-    """
-    Create parser for index slice reference expressions. These expressions are linked
-    to the equation expression by e.g. `$bar` in `foo[bars=$bar]`.
+    """Create parser for index slice reference expressions.
+
+    These expressions are linked to the equation expression by e.g. `$bar` in `foo[bars=$bar]`.
     Unlike sub-expressions and equation expressions, these strings cannot contain arithmetic
     nor references to sub expressions.
 
@@ -1152,9 +1172,9 @@ def generate_slice_parser(valid_component_names: Iterable) -> pp.ParserElement:
 
 
 def generate_sub_expression_parser(valid_component_names: Iterable) -> pp.Forward:
-    """
-    Create parser for sub expressions. These expressions are linked
-    to the equation expression by e.g. `$bar`.
+    """Create parser for sub expressions.
+
+    These expressions are linked to the equation expression by e.g. `$bar`.
     This parser allows arbitrarily nested arithmetic and function calls (and arithmetic inside function calls)
     and reference to index slice expressions.
 
@@ -1187,8 +1207,8 @@ def generate_sub_expression_parser(valid_component_names: Iterable) -> pp.Forwar
 
 
 def generate_arithmetic_parser(valid_component_names: Iterable) -> pp.ParserElement:
-    """
-    Create parser for left-/right-hand side (LHS/RHS) of equation expressions of the form LHS OPERATOR RHS (e.g. `foo == 1 + bar`).
+    """Create parser for arithmetic expressions (+, -, /, *, **).
+
     This parser allows arbitrarily nested arithmetic and function calls (and arithmetic inside function calls)
     and reference to sub-expressions and index slice expressions.
 
@@ -1228,8 +1248,8 @@ def generate_arithmetic_parser(valid_component_names: Iterable) -> pp.ParserElem
 
 
 def generate_equation_parser(valid_component_names: Iterable) -> pp.ParserElement:
-    """
-    Create parser for equation expressions of the form LHS OPERATOR RHS (e.g. `foo == 1 + bar`).
+    """Create parser for equation expressions of the form LHS OPERATOR RHS (e.g. `foo == 1 + bar`).
+
     This parser allows arbitrarily nested arithmetic and function calls (and arithmetic inside function calls)
     and reference to sub-expressions and index slice expressions.
 
@@ -1241,7 +1261,6 @@ def generate_equation_parser(valid_component_names: Iterable) -> pp.ParserElemen
     Returns:
         pp.ParserElement: Parser for expression strings under the constraint key "equation/equations".
     """
-
     arithmetic = generate_arithmetic_parser(valid_component_names)
     equation_comparison = equation_comparison_parser(arithmetic)
 
diff --git a/src/calliope/backend/helper_functions.py b/src/calliope/backend/helper_functions.py
index a2fdc80c..c3f5326f 100644
--- a/src/calliope/backend/helper_functions.py
+++ b/src/calliope/backend/helper_functions.py
@@ -1,8 +1,7 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
 
-"""
-Functions that can be used to process data in math `where` and `expression` strings.
+"""Functions that can be used to process data in math `where` and `expression` strings.
 
 `NAME` is the function name to use in the math strings.
 """
@@ -24,6 +23,8 @@
 
 
 class ParsingHelperFunction(ABC):
+    """Abstract base class for helper function parsing."""
+
     def __init__(
         self,
         return_type: Literal["array", "math_string"],
@@ -46,16 +47,16 @@ def __init__(
     @property
     @abstractmethod
     def ALLOWED_IN(self) -> list[Literal["where", "expression"]]:
-        "List of parseable math strings that this function can be accessed from."
+        """List of parseable math strings that this function can be accessed from."""
 
     @property
     @abstractmethod
     def NAME(self) -> str:
-        "Helper function name that is used in the math expression/where string."
+        """Helper function name that is used in the math expression/where string."""
 
     @property
     def ignore_where(self) -> bool:
-        "If True, `where` arrays will not be applied to the incoming data variables (valid for expression helpers)"
+        """If True, `where` arrays will not be applied to the incoming data variables (valid for expression helpers)."""
         return False
 
     @abstractmethod
@@ -73,9 +74,12 @@ def as_array(self, *args, **kwargs) -> xr.DataArray:
         """
 
     def __call__(self, *args, **kwargs) -> Any:
-        """
-        When a helper function is accessed by evaluating a parsing string, this method is called.
-        The value of `return_type` on initialisation of the class defines whether this method returns a string (``return_type=math_string``) or :meth:xr.DataArray (``return_type=array``)
+        """When a helper function is accessed by evaluating a parsing string, this method is called.
+
+        The value of `return_type` on initialisation of the class defines whether this
+        method returns either:
+        - a string (``return_type=math_string``)
+        - :meth:xr.DataArray (``return_type=array``)
         """
         if self._return_type == "math_string":
             return self.as_math_string(*args, **kwargs)
@@ -83,7 +87,7 @@ def __call__(self, *args, **kwargs) -> Any:
             return self.as_array(*args, **kwargs)
 
     def __init_subclass__(cls):
-        """Override subclass definition in two ways:
+        """Override subclass definition.
 
         1. Do not allow new helper functions to have a name that is already defined (be it a built-in function or a custom function).
         2. Wrap helper function __call__ in a check for the function being allowed in specific parsing string types.
@@ -99,7 +103,7 @@ def __init_subclass__(cls):
 
     @staticmethod
     def _add_to_iterator(instring: str, iterator_converter: dict[str, str]) -> str:
-        """Utility function for generating latex strings in multiple helper functions.
+        r"""Utility function for generating latex strings in multiple helper functions.
 
         Find an iterator in the iterator substring of the component string
         (anything wrapped in `_text{}`). Other parts of the iterator substring can be anything
@@ -156,12 +160,14 @@ def _listify(self, vals: Union[list[str], str]) -> list[str]:
 
 
 class Inheritance(ParsingHelperFunction):
+    """Find all nodes / techs that inherit from a node / tech group."""
+
     #:
     ALLOWED_IN = ["where"]
     #:
     NAME = "inheritance"
 
-    def as_math_string(
+    def as_math_string(  # noqa: D102, override
         self, nodes: Optional[str] = None, techs: Optional[str] = None
     ) -> str:
         strings = []
@@ -253,13 +259,15 @@ def as_array(
 
 
 class WhereAny(ParsingHelperFunction):
+    """Apply `any` over a dimension in `where` string."""
+
     # Class name doesn't match NAME to avoid a clash with typing.Any
     #:
     NAME = "any"
     #:
     ALLOWED_IN = ["where"]
 
-    def as_math_string(self, array: str, *, over: Union[str, list[str]]) -> str:
+    def as_math_string(self, array: str, *, over: Union[str, list[str]]) -> str:  # noqa: D102, override
         if isinstance(over, str):
             overstring = self._instr(over)
         else:
@@ -301,12 +309,14 @@ def as_array(self, parameter: str, *, over: Union[str, list[str]]) -> xr.DataArr
 
 
 class Defined(ParsingHelperFunction):
+    """Find all items of one dimension that are defined in an item of another dimension."""
+
     #:
     NAME = "defined"
     #:
     ALLOWED_IN = ["where"]
 
-    def as_math_string(self, *, within: str, how: Literal["all", "any"], **dims) -> str:
+    def as_math_string(self, *, within: str, how: Literal["all", "any"], **dims) -> str:  # noqa: D102, override
         substrings = []
         for name, vals in dims.items():
             substrings.append(self._latex_substring(how, name, vals, within))
@@ -427,12 +437,13 @@ def _latex_substring(
 
 
 class Sum(ParsingHelperFunction):
-    #:
+    """Apply a summation over dimension(s) in math expressions."""
+
     NAME = "sum"
     #:
     ALLOWED_IN = ["expression"]
 
-    def as_math_string(self, array: str, *, over: Union[str, list[str]]) -> str:
+    def as_math_string(self, array: str, *, over: Union[str, list[str]]) -> str:  # noqa: D102, override
         if isinstance(over, str):
             overstring = self._instr(over)
         else:
@@ -459,12 +470,14 @@ def as_array(
 
 
 class ReduceCarrierDim(ParsingHelperFunction):
+    """Sum over the carrier dimension in math components."""
+
     #:
     NAME = "reduce_carrier_dim"
     #:
     ALLOWED_IN = ["expression"]
 
-    def as_math_string(self, array: str, flow_direction: Literal["in", "out"]) -> str:
+    def as_math_string(self, array: str, flow_direction: Literal["in", "out"]) -> str:  # noqa: D102, override
         return rf"\sum\limits_{{\text{{carrier}} \in \text{{carrier_{flow_direction}}}}} ({array})"
 
     def as_array(
@@ -491,12 +504,14 @@ def as_array(
 
 
 class SelectFromLookupArrays(ParsingHelperFunction):
+    """N-dimensional indexing functionality."""
+
     #:
     NAME = "select_from_lookup_arrays"
     #:
     ALLOWED_IN = ["expression"]
 
-    def as_math_string(self, array: str, **lookup_arrays: str) -> str:
+    def as_math_string(self, array: str, **lookup_arrays: str) -> str:  # noqa: D102, override
         new_strings = {
             (iterator := dim.removesuffix("s")): rf"={array}[{iterator}]"
             for dim, array in lookup_arrays.items()
@@ -516,6 +531,7 @@ def as_array(
             lookup_arrays (dict[str, xr.DataArray]):
                 key: dimension on which to apply vectorised indexing
                 value: array whose values are either NaN or values from the dimension given in the key.
+
         Raises:
             BackendError: `array` must be indexed over the dimensions given in the `lookup_arrays` dict keys.
             BackendError: All `lookup_arrays` must be indexed over all the dimensions given in the `lookup_arrays` dict keys.
@@ -589,12 +605,14 @@ def as_array(
 
 
 class GetValAtIndex(ParsingHelperFunction):
+    """Getter functionality for obtaining values at specific integer indices."""
+
     #:
     NAME = "get_val_at_index"
     #:
     ALLOWED_IN = ["expression", "where"]
 
-    def as_math_string(self, **dim_idx_mapping: str) -> str:
+    def as_math_string(self, **dim_idx_mapping: str) -> str:  # noqa: D102, override
         dim, idx = self._mapping_to_dim_idx(**dim_idx_mapping)
         return f"{dim}[{idx}]"
 
@@ -603,12 +621,10 @@ def as_array(self, **dim_idx_mapping: int) -> xr.DataArray:
 
         This function is primarily useful for timeseries data.
 
-        Keyword Args:
-            key (str): Model dimension in which to extract value.
-            value (int): Integer index of the value to extract (assuming zero-indexing).
-
-        Raises:
-            ValueError: Exactly one dimension:index mapping is expected.
+        Args:
+            **dim_idx_mapping (int): kwargs with
+                key (str): Model dimension in which to extract value.
+                value (int): Integer index of the value to extract (assuming zero-indexing).
 
         Returns:
             xr.DataArray: Dimensionless array containing one value.
@@ -631,15 +647,15 @@ def as_array(self, **dim_idx_mapping: int) -> xr.DataArray:
         dim, idx = self._mapping_to_dim_idx(**dim_idx_mapping)
         return self._input_data.coords[dim][int(idx)]
 
+    # For as_array
     @overload
     @staticmethod
-    def _mapping_to_dim_idx(**dim_idx_mapping: int) -> tuple[str, int]:
-        "used in as_array"
+    def _mapping_to_dim_idx(**dim_idx_mapping: int) -> tuple[str, int]: ...
 
+    # For as_math_string
     @overload
     @staticmethod
-    def _mapping_to_dim_idx(**dim_idx_mapping: str) -> tuple[str, str]:
-        "used in as_math_string"
+    def _mapping_to_dim_idx(**dim_idx_mapping: str) -> tuple[str, str]: ...
 
     @staticmethod
     def _mapping_to_dim_idx(**dim_idx_mapping) -> tuple[str, Union[str, int]]:
@@ -649,6 +665,8 @@ def _mapping_to_dim_idx(**dim_idx_mapping) -> tuple[str, Union[str, int]]:
 
 
 class Roll(ParsingHelperFunction):
+    """Roll (a.k.a. shift) items along ordered dimensions."""
+
     #:
     NAME = "roll"
     #:
@@ -656,9 +674,10 @@ class Roll(ParsingHelperFunction):
 
     @property
     def ignore_where(self) -> bool:
+        """Whether or not to ignore `where` functionality."""
         return True
 
-    def as_math_string(self, array: str, **roll_kwargs: str) -> str:
+    def as_math_string(self, array: str, **roll_kwargs: str) -> str:  # noqa: D102, override
         new_strings = {
             k.removesuffix("s"): f"{-1 * int(v):+d}" for k, v in roll_kwargs.items()
         }
@@ -667,13 +686,14 @@ def as_math_string(self, array: str, **roll_kwargs: str) -> str:
 
     def as_array(self, array: xr.DataArray, **roll_kwargs: int) -> xr.DataArray:
         """Roll (a.k.a., shift) the array along the given dimension(s) by the given number of places.
+
         Rolling keeps the array index labels in the same position, but moves the data by the given number of places.
 
         Args:
             array (xr.DataArray): Array on which to roll data.
-        Keyword Args:
-            key (str): name of dimension on which to roll.
-            value (int): number of places to roll data.
+            **roll_kwargs (int): kwargs with the following
+                key (str): name of dimension on which to roll.
+                value (int): number of places to roll data.
 
         Returns:
             xr.DataArray: `array` with rolled data.
@@ -693,12 +713,14 @@ def as_array(self, array: xr.DataArray, **roll_kwargs: int) -> xr.DataArray:
 
 
 class DefaultIfEmpty(ParsingHelperFunction):
+    """Fill empty (NaN) items in arrays."""
+
     #:
     NAME = "default_if_empty"
     #:
     ALLOWED_IN = ["expression"]
 
-    def as_math_string(self, var: str, default: float | int) -> str:
+    def as_math_string(self, var: str, default: float | int) -> str:  # noqa: D102, override
         return rf"({var}\vee{{}}{default})"
 
     def as_array(self, var: xr.DataArray, default: float | int) -> xr.DataArray:
diff --git a/src/calliope/backend/latex_backend_model.py b/src/calliope/backend/latex_backend_model.py
index f5d16f72..89bc1eeb 100644
--- a/src/calliope/backend/latex_backend_model.py
+++ b/src/calliope/backend/latex_backend_model.py
@@ -1,3 +1,5 @@
+"""LaTeX backend functionality."""
+
 from __future__ import annotations
 
 import logging
@@ -21,8 +23,10 @@
 
 
 class MathDocumentation:
+    """For math documentation."""
+
     def __init__(self) -> None:
-        """Math documentation builder/writer
+        """Math documentation builder/writer.
 
         Args:
             backend_builder (Callable):
@@ -35,9 +39,11 @@ def build(self, include: Literal["all", "valid"] = "all", **kwargs) -> None:
 
         Args:
             include (Literal["all", "valid"], optional):
-                Defines whether to include all possible math equations ("all") or only those for which at least one index item in the "where" string is valid ("valid"). Defaults to "all".
+                Defines whether to include all possible math equations ("all") or only
+                those for which at least one index item in the "where" string is valid
+                ("valid"). Defaults to "all".
+            **kwargs: kwargs for the LaTeX backend.
         """
-
         backend = LatexBackendModel(self._inputs, include=include, **kwargs)
         backend._build()
 
@@ -45,24 +51,26 @@ def build(self, include: Literal["all", "valid"] = "all", **kwargs) -> None:
 
     @property
     def inputs(self):
+        """Getter for backend inputs."""
         return self._inputs
 
     @inputs.setter
     def inputs(self, val: xr.Dataset):
+        """Setter for backend inputs."""
         self._inputs = val
 
+    # Expecting string if not giving filename.
     @overload
     def write(
         self,
         filename: Literal[None] = None,
         mkdocs_tabbed: bool = False,
         format: Optional[_ALLOWED_MATH_FILE_FORMATS] = None,
-    ) -> str:
-        "Expecting string if not giving filename"
+    ) -> str: ...
 
+    # Expecting None (and format arg is not needed) if giving filename.
     @overload
-    def write(self, filename: str | Path, mkdocs_tabbed: bool = False) -> None:
-        "Expecting None (and format arg is not needed) if giving filename"
+    def write(self, filename: str | Path, mkdocs_tabbed: bool = False) -> None: ...
 
     def write(
         self,
@@ -70,18 +78,18 @@ def write(
         mkdocs_tabbed: bool = False,
         format: Optional[_ALLOWED_MATH_FILE_FORMATS] = None,
     ) -> Optional[str]:
-        """_summary_
+        """Write model documentation.
+
+        `build` must be run beforehand.
 
         Args:
             filename (Optional[str], optional):
                 If given, will write the built mathematical formulation to a file with the given extension as the file format. Defaults to None.
-
             format (Optional["tex", "rst", "md"], optional):
                 Not required if filename is given (as the format will be automatically inferred).
                 Required if expecting a string return from calling this function. The LaTeX math will be embedded in a document of the given format (tex=LaTeX, rst=reStructuredText, md=Markdown).
                 Defaults to None.
-
-                mkdocs_tabbed (bool, optional): If True and Markdown docs are being generated, the equations will be on a tab and the original YAML math definition will be on another tab.
+            mkdocs_tabbed (bool, optional): If True and Markdown docs are being generated, the equations will be on a tab and the original YAML math definition will be on another tab.
 
         Raises:
             exceptions.ModelError: Math strings need to be built first (`build`)
@@ -117,6 +125,8 @@ def write(
 
 
 class LatexBackendModel(backend_model.BackendModelGenerator):
+    """Calliope's LaTeX backend."""
+
     # \negthickspace used to counter the introduction of spaces to separate the curly braces
     # in \text. Curly braces need separating otherwise jinja2 gets confused.
     LATEX_EQUATION_ELEMENT = textwrap.dedent(
@@ -347,29 +357,30 @@ def __init__(
         """Interface to build a string representation of the mathematical formulation using LaTeX math notation.
 
         Args:
+            inputs (xr.Dataset): model data.
             include (Literal["all", "valid"], optional):
                 Defines whether to include all possible math equations ("all") or only those for which at least one index item in the "where" string is valid ("valid"). Defaults to "all".
+            **kwargs: for the backend model generator.
         """
         super().__init__(inputs, **kwargs)
         self.include = include
 
         self._add_all_inputs_as_parameters()
 
-    def add_parameter(
+    def add_parameter(  # noqa: D102, override
         self,
         parameter_name: str,
         parameter_values: xr.DataArray,
         default: Any = np.nan,
         use_inf_as_na: bool = False,
     ) -> None:
-
         attrs = {
             "description": self._PARAM_DESCRIPTIONS.get(parameter_name, None),
             "unit": self._PARAM_UNITS.get(parameter_name, None),
         }
         self._add_to_dataset(parameter_name, parameter_values, "parameters", attrs)
 
-    def add_constraint(
+    def add_constraint(  # noqa: D102, override
         self,
         name: str,
         constraint_dict: Optional[parsing.UnparsedConstraintDict] = None,
@@ -390,7 +401,7 @@ def _constraint_setter(
             parsed_component, self.constraints[name], equations=equation_strings
         )
 
-    def add_global_expression(
+    def add_global_expression(  # noqa: D102, override
         self,
         name: str,
         expression_dict: Optional[parsing.UnparsedExpressionDict] = None,
@@ -415,7 +426,7 @@ def _expression_setter(
             parsed_component, self.global_expressions[name], equations=equation_strings
         )
 
-    def add_variable(
+    def add_variable(  # noqa: D102, override
         self, name: str, variable_dict: Optional[parsing.UnparsedVariableDict] = None
     ) -> None:
         domain_dict = {"real": r"\mathbb{R}\;", "integer": r"\mathbb{Z}\;"}
@@ -438,7 +449,7 @@ def _variable_setter(where: xr.DataArray, references: set) -> xr.DataArray:
             parsed_component, where_array, equations=[lb, ub], sense=r"\forall" + domain
         )
 
-    def add_objective(
+    def add_objective(  # noqa: D102, override
         self, name: str, objective_dict: Optional[parsing.UnparsedObjectiveDict] = None
     ) -> None:
         sense_dict = {
@@ -473,7 +484,7 @@ def _create_obj_list(
     ) -> None:
         return None
 
-    def delete_component(
+    def delete_component(  # noqa: D102, override
         self, key: str, component_type: backend_model._COMPONENTS_T
     ) -> None:
         if key in self._dataset and self._dataset[key].obj_type == component_type:
@@ -577,7 +588,7 @@ def _render(template: str, **kwargs) -> str:
         text_starter = r"\\text(?:bf|it)?"  # match one of `\text`, `\textit`, `\textbf`
 
         def __escape_underscore(instring):
-            "KaTeX requires underscores in `\text{...}` blocks to be escaped."
+            r"""KaTeX requires underscores in `\text{...}` blocks to be escaped."""
             return re.sub(
                 rf"{text_starter}{{.*?}}",
                 lambda x: x.group(0).replace("_", r"\_"),
@@ -585,7 +596,7 @@ def __escape_underscore(instring):
             )
 
         def __mathify_text_in_text(instring):
-            """KaTeX requires `\text{...}` blocks within `\text{...}` blocks to be placed within math blocks.
+            r"""KaTeX requires `\text{...}` blocks within `\text{...}` blocks to be placed within math blocks.
 
             We use `\\(` as the math block descriptor.
             """
diff --git a/src/calliope/backend/parsing.py b/src/calliope/backend/parsing.py
index 76255dbf..b6d96942 100644
--- a/src/calliope/backend/parsing.py
+++ b/src/calliope/backend/parsing.py
@@ -1,5 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
+"""Methods for math syntax parsing."""
 
 from __future__ import annotations
 
@@ -33,11 +34,15 @@
 
 
 class UnparsedEquationDict(TypedDict):
+    """Unparsed equation checker class."""
+
     where: NotRequired[str]
     expression: str
 
 
 class UnparsedConstraintDict(TypedDict):
+    """Unparsed constraint checker class."""
+
     description: NotRequired[str]
     foreach: NotRequired[list]
     where: NotRequired[str]
@@ -47,10 +52,14 @@ class UnparsedConstraintDict(TypedDict):
 
 
 class UnparsedExpressionDict(UnparsedConstraintDict):
+    """Unparsed expression checker class."""
+
     unit: NotRequired[str]
 
 
 class UnparsedVariableBoundDict(TypedDict):
+    """Unparsed variable bounds checker class."""
+
     min: str
     max: str
     equals: str
@@ -58,6 +67,8 @@ class UnparsedVariableBoundDict(TypedDict):
 
 
 class UnparsedVariableDict(TypedDict):
+    """Unparsed variable checker class."""
+
     description: NotRequired[str]
     unit: NotRequired[str]
     foreach: list[str]
@@ -67,6 +78,8 @@ class UnparsedVariableDict(TypedDict):
 
 
 class UnparsedObjectiveDict(TypedDict):
+    """Unparsed model objective checker."""
+
     description: NotRequired[str]
     equations: Required[list[UnparsedEquationDict]]
     sub_expressions: NotRequired[dict[str, list[UnparsedEquationDict]]]
@@ -85,6 +98,8 @@ class UnparsedObjectiveDict(TypedDict):
 
 
 class ParsedBackendEquation:
+    """Backend equation parser."""
+
     def __init__(
         self,
         equation_name: str,
@@ -94,9 +109,7 @@ def __init__(
         sub_expressions: Optional[dict[str, pp.ParseResults]] = None,
         slices: Optional[dict[str, pp.ParseResults]] = None,
     ) -> None:
-        """
-        Object for storing a parsed equation expression and corresponding "where" string,
-        with methods to evaluate those elements.
+        """For parsing equation expressions and corresponding "where" strings.
 
         Args:
             equation_name (str): Name of equation.
@@ -143,14 +156,11 @@ def find_sub_expressions(self) -> set[str]:
         return self._find_items_in_expression(elements, to_find, valid_eval_classes)
 
     def find_slices(self) -> set[str]:
-        """
-        Identify all the references to array slices in the parsed expression or in the
-        parsed sub-expressions.
+        """Finds all references to array slices in the expression and sub-expressions.
 
         Returns:
             set[str]: Unique slice references.
         """
-
         valid_eval_classes = tuple(
             [
                 expression_parser.EvalOperatorOperand,
@@ -168,18 +178,17 @@ def find_slices(self) -> set[str]:
 
     @staticmethod
     def _find_items_in_expression(
-        parser_elements: Union[list, pp.ParseResults],
+        parser_elements: list | pp.ParseResults,
         to_find: type[expression_parser.EvalString],
         valid_eval_classes: tuple[type[expression_parser.EvalString], ...],
     ) -> set[str]:
-        """
-        Recursively find sub-expressions / index items defined in an equation expression.
+        """Recursively find sub-expressions / index items defined in an equation expression.
 
         Args:
-            parser_elements (pp.ParseResults): list of parser elements to check.
-            to_find (type[expression_parser.EvalString]): type of equation element to search for
-            valid_eval_classes (tuple[type(expression_parser.EvalString)]):
-                Other expression elements that can be recursively searched
+            parser_elements (list | pp.ParseResults): list of parser elements to check.
+            to_find (type[expression_parser.EvalString]): type of equation element to search for.
+            valid_eval_classes (tuple[type[expression_parser.EvalString], ...]): Other expression
+                elements that can be recursively searched
 
         Returns:
             set[str]: All unique component / index item names.
@@ -206,9 +215,7 @@ def add_expression_group_combination(
         expression_group_name: Literal["sub_expressions", "slices"],
         expression_group_combination: Iterable[ParsedBackendEquation],
     ) -> ParsedBackendEquation:
-        """
-        Add dictionary of parsed sub-expressions/index slices to a copy of self, updating
-        the name and where list of self in the process.
+        """Add parsed sub-expressions/index slices to a copy of self with updated names and were lists.
 
         Args:
             expression_group_name (Literal[sub_expressions, slices]):
@@ -217,8 +224,7 @@ def add_expression_group_combination(
                 All items of expression_group_name to be added.
 
         Returns:
-            ParsedBackendEquation:
-                Copy of self with added sub-expressions/index slice dictionary and updated name
+            ParsedBackendEquation: Copy of self with added sub-expressions/index slice dictionary and updated name
                 and where list to include those corresponding to the dictionary entries.
         """
         new_where_list = [*self.where]
@@ -243,6 +249,7 @@ def add_expression_group_combination(
             },
         )
 
+    # Expecting array if not requesting latex string
     @overload
     def evaluate_where(
         self,
@@ -251,9 +258,9 @@ def evaluate_where(
         return_type: Literal["array"] = "array",
         references: Optional[set] = None,
         initial_where: xr.DataArray = TRUE_ARRAY,
-    ) -> xr.DataArray:
-        "Expecting array if not requesting latex string"
+    ) -> xr.DataArray: ...
 
+    # Expecting string if requesting latex string.
     @overload
     def evaluate_where(
         self,
@@ -261,28 +268,28 @@ def evaluate_where(
         *,
         return_type: Literal["math_string"],
         references: Optional[set] = None,
-    ) -> str:
-        "Expecting string if requesting latex string"
+    ) -> str: ...
 
     def evaluate_where(
         self,
         backend_interface: backend_model.BackendModelGenerator,
         *,
         return_type: str = "array",
-        references: Optional[set] = None,
+        references: set | None = None,
         initial_where: xr.DataArray = TRUE_ARRAY,
     ) -> Union[xr.DataArray, str]:
         """Evaluate parsed backend object dictionary `where` string.
 
         Args:
-            backend_interface (calliope.backend.backend_model.BackendModel): Interface to a optimisation backend.
-
-        Keyword Args:
-            return_type (str, optional):
-                If "array", return xarray.DataArray. If "math_string", return LaTex math string.
+            backend_interface (backend_model.BackendModelGenerator):
+                Interface to an optimisation backend.
+            return_type (str, optional): If "array", return xarray.DataArray.
+                If "math_string", return LaTex math string.
                 Defaults to "array".
-            initial_where (xr.DataArray, optional):
-                If given, the where array resulting from evaluation will be further where'd by this array.
+            references (set | None, optional): List of references to use in evaluation.
+                Defaults to None.
+            initial_where (xr.DataArray, optional): If given, the where array resulting
+                from evaluation will be further where'd by this array.
                 Defaults to xr.DataArray(True) (i.e., no effect).
 
         Returns:
@@ -313,7 +320,7 @@ def evaluate_where(
             return where
 
     def drop_dims_not_in_foreach(self, where: xr.DataArray) -> xr.DataArray:
-        """the dimensions not included in "foreach" are removed from the input array
+        """Remove all dimensions not included in "foreach" from the input array.
 
         Args:
             where (xr.DataArray): Array with potentially unwanted dimensions
@@ -326,6 +333,7 @@ def drop_dims_not_in_foreach(self, where: xr.DataArray) -> xr.DataArray:
         unwanted_dims = set(where.dims).difference(self.sets)
         return (where.sum(unwanted_dims) > 0).astype(bool).transpose(*self.sets)
 
+    # Expecting anything (most likely an array) if not requesting latex string.
     @overload
     def evaluate_expression(
         self,
@@ -334,9 +342,9 @@ def evaluate_expression(
         return_type: Literal["array"] = "array",
         references: Optional[set] = None,
         where: xr.DataArray = TRUE_ARRAY,
-    ) -> xr.DataArray:
-        "Expecting anything (most likely an array) if not requesting latex string"
+    ) -> xr.DataArray: ...
 
+    # Expecting string if requesting latex string.
     @overload
     def evaluate_expression(
         self,
@@ -344,8 +352,7 @@ def evaluate_expression(
         *,
         return_type: Literal["math_string"],
         references: Optional[set] = None,
-    ) -> str:
-        "Expecting string if requesting latex string"
+    ) -> str: ...
 
     def evaluate_expression(
         self,
@@ -355,22 +362,28 @@ def evaluate_expression(
         references: Optional[set] = None,
         where: xr.DataArray = TRUE_ARRAY,
     ) -> Union[xr.DataArray, str]:
-        """Evaluate a math string to produce either an array of backend objects or a LaTex math string.
+        """Evaluate a math string to produce an array backend objects or a LaTex math string.
 
         Args:
-            backend_interface (calliope.backend.backend_model.BackendModel): Interface to a optimisation backend.
+            backend_interface (calliope.backend.backend_model.BackendModel):
+                Interface to a optimisation backend.
 
         Keyword Args:
             return_type (str, optional):
                 If "array", return xarray.DataArray. If "math_string", return LaTex math string.
                 Defaults to "array".
-            references (Optional[set], optional): If given, any references in the math string to other model components will be logged here. Defaults to None.
-            where (xr.DataArray, optional): If given, should be a boolean array with which to mask any produced arrays. Defaults to xr.DataArray(True).
+            references (Optional[set], optional):
+                If given, any references in the math string to other model components
+                will be logged here. Defaults to None.
+            where (xr.DataArray, optional):
+                If given, should be a boolean array with which to mask any produced arrays.
+                Defaults to xr.DataArray(True).
 
         Returns:
             Union[xr.DataArray, str]:
                 If return_type == `array`: array of backend expression objects.
-                If return_type == `math_string`: Valid LaTeX math string defining the "where" conditions using logic notation.
+                If return_type == `math_string`: Valid LaTeX math string defining the
+                "where" conditions using logic notation.
         """
         evaluated = self.expression[0].eval(
             return_type,
@@ -390,8 +403,7 @@ def evaluate_expression(
     def raise_error_on_where_expr_mismatch(
         self, expression: xr.DataArray, where: xr.DataArray
     ) -> None:
-        """
-        Checks if an evaluated expression is consistent with the `where` array.
+        """Checks if an evaluated expression is consistent with the `where` array.
 
         Args:
             expression (xr.DataArray): array of linear expressions or one side of a constraint equation.
@@ -420,11 +432,12 @@ def log_not_added(
         message: str,
         level: Literal["info", "warning", "debug", "error", "critical"] = "debug",
     ):
-        """Log to module-level logger with some prettification of the message
+        """Log to module-level logger with some prettification of the message.
 
         Args:
             message (str): Message to log.
-            level (Literal["info", "warning", "debug", "error", "critical"], optional): Log level. Defaults to "debug".
+            level (Literal["info", "warning", "debug", "error", "critical"], optional):
+                Log level. Defaults to "debug".
         """
         getattr(LOGGER, level)(
             f"Math parsing | {self.name} | Component not added; {message}"
@@ -432,6 +445,8 @@ def log_not_added(
 
 
 class ParsedBackendComponent(ParsedBackendEquation):
+    """Backend component parser."""
+
     _ERR_BULLET: str = " * "
     _ERR_STRING_ORDER: list[str] = ["expression_group", "id", "expr_or_where"]
     PARSERS: dict[str, Callable] = {
@@ -447,15 +462,17 @@ def __init__(
         name: str,
         unparsed_data: T,
     ) -> None:
-        """
-        Parse an optimisation problem configuration - defined in a dictionary of strings
-        loaded from YAML - into a series of Python objects that can be passed onto a solver
-        interface like Pyomo or Gurobipy.
+        """Parse an optimisation problem configuration.
+
+        Defined in a dictionary of strings loaded from YAML into a series of Python
+        objects that can be passed onto a solver interface like Pyomo or Gurobipy.
 
         Args:
-            group (Literal["variables", "global_expressions", "constraints", "objectives"]): Optimisation problem component group to which the unparsed data belongs.
+            group (Literal["variables", "global_expressions", "constraints", "objectives"]):
+                Optimisation problem component group to which the unparsed data belongs.
             name (str): Name of the optimisation problem component
-            unparsed_data (T): Unparsed math formulation. Expected structure depends on the group to which the optimisation problem component belongs.
+            unparsed_data (T): Unparsed math formulation. Expected structure depends on
+                the group to which the optimisation problem component belongs.
         """
         self.name = f"{group}:{name}"
         self._unparsed: dict = dict(unparsed_data)
@@ -477,17 +494,17 @@ def __init__(
         self.sets: list[str] = unparsed_data.get("foreach", [])  # type:ignore
 
     def get_parsing_position(self):
-        """Create "." separated list from tracked strings"""
+        """Create "." separated list from tracked strings."""
         return ".".join(
             filter(None, [self._tracker[i] for i in self._ERR_STRING_ORDER])
         )
 
     def reset_tracker(self):
-        """Re-initialise error string tracking"""
+        """Re-initialise error string tracking."""
         self._tracker = self._init_tracker()
 
     def _init_tracker(self):
-        "Initialise error string tracking as dictionary of `key: None`"
+        """Initialise error string tracking as dictionary of `key: None`."""
         return {i: None for i in self._ERR_STRING_ORDER}
 
     def parse_top_level_where(
@@ -583,9 +600,7 @@ def parse_equations(
     def _parse_string(
         self, parser: pp.ParserElement, parse_string: str
     ) -> pp.ParseResults:
-        """
-        Parse equation string according to predefined string parsing grammar
-        given by `self.parser`
+        """Parse equation string according to predefined parsing grammar.
 
         Args:
             parser (pp.ParserElement): Parsing grammar.
@@ -610,8 +625,9 @@ def _parse_string(
         return parsed
 
     def parse_where_string(self, where_string: str = "True") -> pp.ParseResults:
-        """Parse a "where" string of the form "CONDITION OPERATOR CONDITION", where the
-        operator can be "and"/"or"/"not and"/"not or".
+        """Parse a "where" string of the form "CONDITION OPERATOR CONDITION".
+
+        The operator can be "and"/"or"/"not and"/"not or".
 
         Args:
             where_string (str):
@@ -633,24 +649,25 @@ def generate_expression_list(
         expression_group: Literal["equations", "sub_expressions", "slices"],
         id_prefix: str = "",
     ) -> list[ParsedBackendEquation]:
-        """
-        Align user-defined constraint equations/sub-expressions by parsing expressions,
-        specifying a default "where" string if not defined,
-        and providing an ID to enable returning to the initial dictionary.
+        """Align user-defined constraint equations/sub-expressions.
+
+        Achieved by parsing expressions, specifying a default "where" string if not
+        defined, and providing an ID to enable returning to the initial dictionary.
 
         Args:
-            expression_list (list[dict]):
-                list of constraint equations or sub-expressions with arithmetic expression
-                string and optional where string.
-            expression_group (str):
+            expression_parser (pp.ParserElement): parser to use.
+            expression_list (list[UnparsedEquationDict]): list of constraint equations
+                or sub-expressions with arithmetic expression string and optional
+                where string.
+            expression_group (Literal["equations", "sub_expressions", "slices"]):
                 For error reporting, the constraint dict key corresponding to the parse_string.
-            id_prefix (Optional[str]):
-                If provided, will extend the ID from a number corresponding to the
+            id_prefix (str, optional): Extends the ID from a number corresponding to the
                 expression_list position `idx` to a tuple of the form (id_prefix, idx).
+                Defaults to "".
 
         Returns:
-            list[ParsedBackendEquation]:
-                Aligned expression dictionaries with parsed expression strings.
+            list[ParsedBackendEquation]: Aligned expression dictionaries with parsed
+                expression strings.
         """
         parsed_equation_list = []
 
@@ -690,21 +707,24 @@ def extend_equation_list_with_expression_group(
         parsed_items: dict[str, list[ParsedBackendEquation]],
         expression_group: Literal["sub_expressions", "slices"],
     ) -> list[ParsedBackendEquation]:
-        """
-        Find all sub-expressions referenced in an equation expression and return a
+        """Extend equation expressions with sub-expression data.
+
+        Finds all sub-expressions referenced in an equation expression and returns a
         product of the sub-expression data.
 
         Args:
-            equation_data (ParsedBackendEquation): Equation data dictionary.
+            parsed_equation (ParsedBackendEquation): Equation data dictionary.
             parsed_items (dict[str, list[ParsedBackendEquation]]):
                 Dictionary of expressions to replace within the equation data dictionary.
             expression_group (Literal["sub_expressions", "slices"]):
                 Name of expression group that the parsed_items dict is referencing.
 
         Returns:
-            list[ParsedBackendEquation]:
-                Expanded list of parsed equations with the product of all references to items from the `expression_group` producing a new equation object.
-                E.g., if the input equation object has a reference to an slice which itself has two expression options, two equation objects will be added to the return list.
+            list[ParsedBackendEquation]: Expanded list of parsed equations with the
+                product of all references to items from the `expression_group`
+                producing a new equation object. E.g., if the input equation object has
+                a reference to an slice which itself has two expression options, two
+                equation objects will be added to the return list.
         """
         if expression_group == "sub_expressions":
             equation_items = parsed_equation.find_sub_expressions()
@@ -733,16 +753,23 @@ def extend_equation_list_with_expression_group(
     def combine_definition_matrix_and_foreach(
         self, input_data: xr.Dataset
     ) -> xr.DataArray:
-        """Generate a multi-dimensional boolean array based on the sets over which the constraint is to be built (defined by "foreach") and the model `exists` array.
+        """Generate a multi-dimensional array where a constraint will be built.
+
+        The multi-dimensional boolean array is based on the sets over which the
+        constraint is to be built (`foreach`) and the model `exists` array.
+
+        The `exists` array is a boolean array defining the structure of the model and
+        is True for valid combinations of technologies consuming/producing specific
+        carriers at specific nodes.
 
-        The `exists` array is a boolean array defining the structure of the model and is True for valid combinations of technologies consuming/producing specific carriers at specific nodes.
         It is indexed over ["nodes", "techs", "carriers"].
 
         Args:
             input_data (xr.Dataset): Calliope model dataset.
 
         Returns:
-            xr.DataArray: boolean array indexed over ["nodes", "techs", "carriers"] + any additional dimensions provided by `foreach`.
+            xr.DataArray: boolean array indexed over ["nodes", "techs", "carriers"]
+                + any additional dimensions provided by `foreach`.
         """
         # Start with (carriers, nodes, techs) and go from there
         exists = input_data.definition_matrix
@@ -762,21 +789,26 @@ def generate_top_level_where_array(
         *,
         align_to_foreach_sets: bool = True,
         break_early: bool = True,
-        references: Optional[set] = None,
+        references: set | None = None,
     ) -> xr.DataArray:
-        """
-        Create multi-dimensional array from model inputs and component sets (defined in foreach)
-        and apply the component top-level where to the array.
+        """Generate a multi-dimentional "where" array.
+
+        The multi-dimensional array is created using model inputs and component sets
+        defined in foreach. The component top-level "where" is then applied to the
+        array.
 
         Args:
-            backend_interface (calliope.backend.backend_model.BackendModel): Interface to a optimisation backend.
+            backend_interface (backend_model.BackendModel): Interface to a optimisation backend.
             align_to_foreach_sets (bool, optional):
-                By default, all foreach arrays have the dimensions ("nodes", "techs", "carriers") as well as any additional dimensions provided by the component's "foreach" key.
+                By default, all foreach arrays have the dimensions ("nodes", "techs", "carriers")
+                as well as any additional dimensions provided by the component's "foreach" key.
                 If this argument is True, the dimensions not included in "foreach" are removed from the array.
                 Defaults to True.
             break_early (bool, optional):
-                If any intermediate array has no valid elements (i.e. all are False), the function will return that array rather than continuing - this saves time and memory on large models.
-                Defaults to True.
+                If any intermediate array has no valid elements (i.e. all are False),
+                the function will return that array rather than continuing - saving
+                time and memory on large models. Defaults to True.
+            references (set | None, optional): references to use during evaluation. Defaults to None.
 
         Returns:
             xr.DataArray: Boolean array defining on which index items a parsed component should be built.
@@ -804,10 +836,13 @@ def generate_top_level_where_array(
         return where
 
     def raise_caught_errors(self):
-        """If there are any parsing errors, pipe them to the ModelError bullet point list generator"""
+        """If there are any parsing errors, pipe them to the ModelError bullet point list generator."""
         if not self._is_valid:
             exceptions.print_warnings_and_raise_errors(
                 errors={f"{self.name}": self._errors},
-                during="math string parsing (marker indicates where parsing stopped, which might not be the root cause of the issue; sorry...)",
+                during=(
+                    "math string parsing (marker indicates where parsing stopped, "
+                    "which might not be the root cause of the issue; sorry...)"
+                ),
                 bullet=self._ERR_BULLET,
             )
diff --git a/src/calliope/backend/pyomo_backend_model.py b/src/calliope/backend/pyomo_backend_model.py
index e0379f2b..452a3847 100644
--- a/src/calliope/backend/pyomo_backend_model.py
+++ b/src/calliope/backend/pyomo_backend_model.py
@@ -1,5 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
+"""Pyomo backend functionality."""
 
 from __future__ import annotations
 
@@ -51,12 +52,14 @@
 
 
 class PyomoBackendModel(backend_model.BackendModel):
+    """Pyomo-specific backend functionality."""
 
     def __init__(self, inputs: xr.Dataset, **kwargs) -> None:
         """Pyomo solver interface class.
 
         Args:
             inputs (xr.Dataset): Calliope model data.
+            **kwargs: passed directly to the solver.
         """
         super().__init__(inputs, pmo.block(), **kwargs)
 
@@ -71,7 +74,7 @@ def __init__(self, inputs: xr.Dataset, **kwargs) -> None:
 
         self._add_all_inputs_as_parameters()
 
-    def add_parameter(
+    def add_parameter(  # noqa: D102, override
         self,
         parameter_name: str,
         parameter_values: xr.DataArray,
@@ -106,7 +109,7 @@ def add_parameter(
         }
         self._add_to_dataset(parameter_name, parameter_da, "parameters", attrs)
 
-    def add_constraint(
+    def add_constraint(  # noqa: D102, override
         self,
         name: str,
         constraint_dict: Optional[parsing.UnparsedConstraintDict] = None,
@@ -131,7 +134,7 @@ def _constraint_setter(
 
         self._add_component(name, constraint_dict, _constraint_setter, "constraints")
 
-    def add_global_expression(
+    def add_global_expression(  # noqa: D102, override
         self,
         name: str,
         expression_dict: Optional[parsing.UnparsedExpressionDict] = None,
@@ -152,7 +155,7 @@ def _expression_setter(
             name, expression_dict, _expression_setter, "global_expressions"
         )
 
-    def add_variable(
+    def add_variable(  # noqa: D102, override
         self, name: str, variable_dict: Optional[parsing.UnparsedVariableDict] = None
     ) -> None:
         domain_dict = {"real": pmo.RealSet, "integer": pmo.IntegerSet}
@@ -174,7 +177,7 @@ def _variable_setter(where, references):
 
         self._add_component(name, variable_dict, _variable_setter, "variables")
 
-    def add_objective(
+    def add_objective(  # noqa: D102, override
         self, name: str, objective_dict: Optional[parsing.UnparsedObjectiveDict] = None
     ) -> None:
         sense_dict = {"minimize": 1, "minimise": 1, "maximize": -1, "maximise": -1}
@@ -201,7 +204,9 @@ def _objective_setter(
 
         self._add_component(name, objective_dict, _objective_setter, "objectives")
 
-    def get_parameter(self, name: str, as_backend_objs: bool = True) -> xr.DataArray:
+    def get_parameter(  # noqa: D102, override
+        self, name: str, as_backend_objs: bool = True
+    ) -> xr.DataArray:
         parameter = self.parameters.get(name, None)
         if parameter is None:
             raise KeyError(f"Unknown parameter: {name}")
@@ -221,7 +226,7 @@ def get_parameter(self, name: str, as_backend_objs: bool = True) -> xr.DataArray
             )
             return param_as_vals.astype(parameter.original_dtype)
 
-    def get_constraint(
+    def get_constraint(  # noqa: D102, override
         self, name: str, as_backend_objs: bool = True, eval_body: bool = False
     ) -> Union[xr.DataArray, xr.Dataset]:
         constraint = self.constraints.get(name, None)
@@ -238,7 +243,9 @@ def get_constraint(
             constraint = constraint_attrs.to_dataset("attributes")
         return constraint
 
-    def get_variable(self, name: str, as_backend_objs: bool = True) -> xr.DataArray:
+    def get_variable(  # noqa: D102, override
+        self, name: str, as_backend_objs: bool = True
+    ) -> xr.DataArray:
         variable = self.variables.get(name, None)
         if variable is None:
             raise KeyError(f"Unknown variable: {name}")
@@ -247,7 +254,9 @@ def get_variable(self, name: str, as_backend_objs: bool = True) -> xr.DataArray:
         else:
             return self._apply_func(self._from_pyomo_param, variable)
 
-    def get_variable_bounds(self, name: str) -> xr.Dataset:
+    def get_variable_bounds(  # noqa: D102, override
+        self, name: str
+    ) -> xr.Dataset:
         variable = self.get_variable(name, as_backend_objs=True)
         variable_attrs = self._apply_func(
             self._from_pyomo_variable_bounds,
@@ -257,7 +266,7 @@ def get_variable_bounds(self, name: str) -> xr.Dataset:
         variable_attrs.coords["attributes"] = ["lb", "ub"]
         return variable_attrs.to_dataset("attributes")
 
-    def get_global_expression(
+    def get_global_expression(  # noqa: D102, override
         self, name: str, as_backend_objs: bool = True, eval_body: bool = False
     ) -> xr.DataArray:
         global_expression = self.global_expressions.get(name, None)
@@ -270,7 +279,7 @@ def get_global_expression(
         else:
             return global_expression
 
-    def _solve(
+    def _solve(  # noqa: D102, override
         self,
         solver: str,
         solver_io: Optional[str] = None,
@@ -329,7 +338,7 @@ def _solve(
 
         return results
 
-    def verbose_strings(self) -> None:
+    def verbose_strings(self) -> None:  # noqa: D102, override
         def __renamer(val, *idx):
             if pd.notnull(val):
                 val.calliope_coords = idx
@@ -342,7 +351,9 @@ def __renamer(val, *idx):
                     self._apply_func(__renamer, da, *[da.coords[i] for i in da.dims])
                     da.attrs["coords_in_name"] = True
 
-    def to_lp(self, path: Union[str, Path]) -> None:
+    def to_lp(  # noqa: D102, override
+        self, path: Union[str, Path]
+    ) -> None:
         self._instance.write(str(path), format="lp", symbolic_solver_labels=True)
 
     def _create_obj_list(self, key: str, component_type: _COMPONENTS_T) -> None:
@@ -366,13 +377,7 @@ def _create_obj_list(self, key: str, component_type: _COMPONENTS_T) -> None:
                 pmo, f"{COMPONENT_TRANSLATOR[singular_component]}_list"
             )()
 
-    def delete_component(self, key: str, component_type: _COMPONENTS_T) -> None:
-        """Delete a list object from the backend model object.
-
-        Args:
-            key (str): Name of object
-            component_type (str): Object type
-        """
+    def delete_component(self, key: str, component_type: _COMPONENTS_T) -> None:  # noqa: D102, override
         component_dict = getattr(self._instance, component_type)
         if key in component_dict:
             del component_dict[key]
@@ -380,7 +385,7 @@ def delete_component(self, key: str, component_type: _COMPONENTS_T) -> None:
         if key in self._dataset and self._dataset[key].obj_type == component_type:
             del self._dataset[key]
 
-    def update_parameter(
+    def update_parameter(  # noqa: D102, override
         self, name: str, new_values: Union[xr.DataArray, SupportsFloat]
     ) -> None:
         new_values = xr.DataArray(new_values)
@@ -432,7 +437,7 @@ def update_parameter(
 
         self._apply_func(self._update_pyomo_param, parameter_da, new_values)
 
-    def update_variable_bounds(
+    def update_variable_bounds(  # noqa: D102, override
         self,
         name: str,
         *,
@@ -476,20 +481,26 @@ def update_variable_bounds(
                 bound=translator[bound_name],
             )
 
-    def fix_variable(self, name: str, where: Optional[xr.DataArray] = None) -> None:
+    def fix_variable(  # noqa: D102, override
+        self, name: str, where: Optional[xr.DataArray] = None
+    ) -> None:
         variable_da = self.get_variable(name)
         if where is not None:
             variable_da = variable_da.where(where.fillna(0))
         self._apply_func(self._fix_pyomo_variable, variable_da)
 
-    def unfix_variable(self, name: str, where: Optional[xr.DataArray] = None) -> None:
+    def unfix_variable(  # noqa: D102, override
+        self, name: str, where: Optional[xr.DataArray] = None
+    ) -> None:
         variable_da = self.get_variable(name)
         if where is not None:
             variable_da = variable_da.where(where.fillna(0))
         self._apply_func(self._unfix_pyomo_variable, variable_da)
 
     @property
-    def has_integer_or_binary_variables(self) -> bool:
+    def has_integer_or_binary_variables(  # noqa: D102, override
+        self,
+    ) -> bool:
         model_report = build_model_size_report(self._instance)
         binaries = model_report["activated"]["binary_variables"]
         integers = model_report["activated"]["integer_variables"]
@@ -499,18 +510,18 @@ def has_integer_or_binary_variables(self) -> bool:
     def _get_capacity_bound(
         self, bound: Any, name: str, references: set
     ) -> xr.DataArray:
-        """
-        Generate array for the upper/lower bound of a decision variable.
+        """Generate array for the upper/lower bound of a decision variable.
+
         Any NaN values will be replaced by None, which Pyomo will correctly interpret as there being no bound to apply.
 
         Args:
             bound (Any): The bound name (corresponding to an array in the model input data) or value.
             name (str): Name of decision variable.
+            references (set): references to use for the array generation.
 
         Returns:
             xr.DataArray: Where unbounded, the array entry will be None, otherwise a float value.
         """
-
         if isinstance(bound, str):
             self.log(
                 "variables",
@@ -527,9 +538,8 @@ def _get_capacity_bound(
     def _to_pyomo_param(
         self, val: Any, *, name: str, default: Any = np.nan, use_inf_as_na: bool = True
     ) -> Union[type[ObjParameter], float]:
-        """
-        Utility function to generate a pyomo parameter for every element of an
-        xarray DataArray.
+        """Utility function to generate a pyomo parameter for every element of an xarray DataArray.
+
         Output objects are of the type ObjParameter(pmo.parameter) since they need a
         "dtype" property to be handled by xarray.
 
@@ -578,15 +588,17 @@ def _update_pyomo_variable(
 
         Args:
             orig (ObjVariable): Pyomo variable to update.
-        Keyword Args:
-            lb (Any): Value with which to update the lower bound of the variable.
-            ub (Any): Value with which to update the upper bound of the variable.
+            new (Any): new value to set.
+            bound (Literal["lb", "ub"]): upper / lower bound.
+                lb (Any): Value with which to update the lower bound of the variable.
+                ub (Any): Value with which to update the upper bound of the variable.
         """
         if pd.notnull(orig) and pd.notnull(new):
             setattr(orig, bound, new)
 
     def _fix_pyomo_variable(self, orig: ObjVariable) -> None:
         """Utility function to fix a pyomo variable to its value in the optimisation model solution.
+
         Fixed variables will be considered as parameters in the subsequent solve.
 
         Args:
@@ -617,9 +629,7 @@ def _unfix_pyomo_variable(self, orig: ObjVariable) -> None:
     def _to_pyomo_constraint(
         self, mask: Union[bool, np.bool_], expr: Any, *, name: str
     ) -> Union[type[ObjConstraint], float]:
-        """
-        Utility function to generate a pyomo constraint for every element of an
-        xarray DataArray.
+        """Utility function to generate a pyomo constraint for every element of an xarray DataArray.
 
         If not np.nan/None, output objects are also added to the backend model object in-place.
 
@@ -635,7 +645,6 @@ def _to_pyomo_constraint(
                 If mask is True, return np.nan.
                 Otherwise return pmo_constraint(expr=lhs op rhs).
         """
-
         if mask:
             if isinstance(expr, (np.bool_, bool)):
                 raise BackendError(
@@ -651,13 +660,10 @@ def _to_pyomo_constraint(
     def _to_pyomo_expression(
         self, mask: Union[bool, np.bool_], expr: Any, *, name: str
     ) -> Union[type[pmo.expression], float]:
-        """
-        Utility function to generate a pyomo expression for every element of an
-        xarray DataArray.
+        """Utility function to generate a pyomo expression for every element of an xarray DataArray.
 
         If not np.nan/None, output objects are also added to the backend model object in-place.
 
-
         Args:
             mask (Union[bool, np.bool_]): If True, add expression, otherwise return np.nan.
             expr (Any): Linear expression to add.
@@ -685,9 +691,7 @@ def _to_pyomo_variable(
         name: str,
         domain_type: Literal["RealSet", "IntegerSet"],
     ) -> Union[type[ObjVariable], float]:
-        """
-        Utility function to generate a pyomo decision variable for every element of an
-        xarray DataArray.
+        """Utility function to generate a pyomo decision variable for every element of an xarray DataArray.
 
         If not np.nan/None, output objects are also added to the backend model object in-place.
 
@@ -715,8 +719,8 @@ def _to_pyomo_variable(
 
     @staticmethod
     def _from_pyomo_param(val: Union[ObjParameter, ObjVariable, float]) -> Any:
-        """
-        Evaluate value of Pyomo object.
+        """Evaluate value of Pyomo object.
+
         If the input object is a parameter, a numeric/string value will be given.
         If the input object is a global expression or variable, a numeric value will be given
         only if the backend model has been successfully optimised, otherwise evaluation will return None.
@@ -831,14 +835,14 @@ def _datetime_as_string(self, data: Union[xr.DataArray, xr.Dataset]) -> Iterator
 
 
 class CoordObj(ABC):
-    """Class with methods to update the `name` property of inheriting classes"""
+    """Generic class for name updates."""
 
     def __init__(self) -> None:
+        """Class with methods to update the `name` property of inheriting classes."""
         self._calliope_coords: Optional[Iterable] = None
 
     def _update_name(self, old_name: str) -> str:
-        """
-        Update string of a list containing a single number with a string of a list containing any arbitrary number of elements
+        """Update string of a list containing a single number with a string of a list containing any arbitrary number of elements.
 
         Args:
             old_name (str): String representation of a list containing a single number
@@ -859,73 +863,90 @@ def _update_name(self, old_name: str) -> str:
 
     @property
     def calliope_coords(self):
+        """Get coordinates."""
         return self._calliope_coords
 
     @calliope_coords.setter
     def calliope_coords(self, val):
+        """Set coordinates."""
         self._calliope_coords = val
 
 
 class ObjParameter(pmo.parameter, CoordObj):
-    """
-    A pyomo parameter (`a object for storing a mutable, numeric value that can be used to build a symbolic expression`)
-    with added `dtype` property and a `name` property setter (via the `pmo.parameter.getname` method) which replaces a list position as a name with a list of strings.
-    """
+    """Pyomo parameter functionality."""
 
     def __init__(self, value, **kwds):
+        """Instantiate a pyomo Parameter.
+
+        A pyomo parameter (`a object for storing a mutable, numeric value that can be used to build a symbolic expression`)
+        with added `dtype` property and a `name` property setter (via the `pmo.parameter.getname` method) which replaces a list position as a name with a list of strings.
+        """
         assert not pd.isnull(value)
         pmo.parameter.__init__(self, value, **kwds)
         CoordObj.__init__(self)
 
     @property
     def dtype(self):
+        """Get dtype."""
         return "O"
 
     def getname(self, *args, **kwargs):
+        """Get name."""
         return self._update_name(pmo.parameter.getname(self, *args, **kwargs))
 
 
 class ObjVariable(pmo.variable, CoordObj):
-    """
-    A pyomo variable with a `name` property setter (via the `pmo.variable.getname` method) which replaces a list position as a name with a list of strings.
-
-    """
+    """Pyomo variable functionality."""
 
     def __init__(self, **kwds):
+        """Create a pyomo variable.
+
+        Created with a `name` property setter (via the `pmo.variable.getname` method)
+        which replaces a list position as a name with a list of strings.
+        """
         pmo.variable.__init__(self, **kwds)
         CoordObj.__init__(self)
 
     def getname(self, *args, **kwargs):
+        """Get variable name."""
         return self._update_name(pmo.variable.getname(self, *args, **kwargs))
 
 
 class ObjConstraint(pmo.constraint, CoordObj):
-    """
-    A pyomo constraint with a `name` property setter (via the `pmo.constraint.getname` method) which replaces a list position as a name with a list of strings.
-
-    """
+    """Pyomo constraint functionality."""
 
     def __init__(self, **kwds):
+        """Create a pyomo constraint.
+
+        Created with a `name` property setter (via the `pmo.constraint.getname` method)
+        which replaces a list position as a name with a list of strings.
+        """
         pmo.constraint.__init__(self, **kwds)
         CoordObj.__init__(self)
 
     def getname(self, *args, **kwargs):
+        """Get constraint name."""
         return self._update_name(pmo.constraint.getname(self, *args, **kwargs))
 
 
 class PyomoShadowPrices(backend_model.ShadowPrices):
+    """Pyomo shadowprice functionality."""
+
     def __init__(self, dual_obj: pmo.suffix, backend_obj: PyomoBackendModel):
+        """Create deactivated pyomo shadowprice functions."""
         self._dual_obj = dual_obj
         self._backend_obj = backend_obj
         self.deactivate()
 
     def get(self, name: str) -> xr.DataArray:
+        """Get shadowprices of a specific constraint."""
         constraint = self._backend_obj.get_constraint(name, as_backend_objs=True)
         return self._backend_obj._apply_func(
             self._duals_from_pyomo_constraint, constraint, dual_getter=self._dual_obj
         )
 
     def activate(self):
+        """Activate shadowprice functionality."""
         if self._backend_obj.has_integer_or_binary_variables:
             warning_text = "Shadow price tracking on a model with binary or integer variables is not possible. Proceeding without activating shadow price tracking."
             model_warn(warning_text, _class=BackendWarning)
@@ -933,14 +954,17 @@ def activate(self):
             self._dual_obj.activate()
 
     def deactivate(self):
+        """Deactivate shadowprice functionality."""
         self._dual_obj.deactivate()
 
     @property
     def is_active(self) -> bool:
+        """Check if shadowprice functionality is enabled."""
         return self._dual_obj.active
 
     @property
     def available_constraints(self) -> Iterable:
+        """Get available constraints."""
         return self._backend_obj.constraints.data_vars
 
     @staticmethod
diff --git a/src/calliope/backend/where_parser.py b/src/calliope/backend/where_parser.py
index 755bbf9d..fdc94f8f 100644
--- a/src/calliope/backend/where_parser.py
+++ b/src/calliope/backend/where_parser.py
@@ -1,5 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
+"""Parsing for 'where' statements."""
 
 from __future__ import annotations
 
@@ -24,6 +25,8 @@
 
 
 class EvalAttrs(TypedDict):
+    """Fixed dict checker for `eval_attrs`."""
+
     equation_name: str
     backend_interface: BackendModel
     input_data: xr.Dataset
@@ -33,27 +36,27 @@ class EvalAttrs(TypedDict):
 
 
 class EvalWhere(expression_parser.EvalToArrayStr):
-    "Update type reference for `eval_attrs` to match `where` evaluation kwargs"
+    """Update type reference for `eval_attrs` to match `where` evaluation kwargs."""
+
     eval_attrs: EvalAttrs = {}
 
 
 class EvalNot(EvalWhere, expression_parser.EvalSignOp):
-    "Parse action to process successfully parsed expressions with a leading `not`."
+    """Parse action to process successfully parsed expressions with a leading `not`."""
 
-    def as_math_string(self) -> str:
-        """Add sign to stringified data for use in a LaTex math formula"""
+    def as_math_string(self) -> str:  # noqa: D102, override
         evaluated = self.value.eval("math_string", **self.eval_attrs)
         return rf"\neg ({evaluated})"
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         evaluated = self.value.eval("array", **self.eval_attrs)
         return ~evaluated
 
 
 class EvalAndOr(EvalWhere, expression_parser.EvalOperatorOperand):
-    """
-    Parse action to process successfully parsed expressions with operands separated
-    by an and/or operator (OPERAND OPERATOR OPERAND OPERATOR OPERAND ...)
+    """Processing of successfully parsed expressions with and/or operators.
+
+    E.g., "OPERAND OPERATOR OPERAND OPERATOR OPERAND ..."
     """
 
     LATEX_OPERATOR_LOOKUP: dict[str, str] = {
@@ -69,7 +72,7 @@ def _skip_component_on_conditional(self, component: str, operator_: str) -> bool
     def _operate(
         val: xr.DataArray, evaluated_operand: xr.DataArray, operator_: str
     ) -> xr.DataArray:
-        "Apply bitwise comparison between boolean xarray dataarrays."
+        """Apply bitwise comparison between boolean xarray dataarrays."""
         match operator_:
             case "and":
                 val = operator.and_(val, evaluated_operand)
@@ -78,20 +81,21 @@ def _operate(
         return val
 
     def _apply_where_array(self, evaluated: xr.DataArray) -> xr.DataArray:
-        "Override func from parent class to effectively do nothing."
+        """Override func from parent class to effectively do nothing."""
         return evaluated
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         return super().as_math_string()
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         return super().as_array()
 
 
 class ConfigOptionParser(EvalWhere):
+    """Parsing of configuration options."""
+
     def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed configuration option names.
+        """Parse action to process successfully parsed configuration option names.
 
         Args:
             instring (str): String that was parsed (used in error message).
@@ -106,12 +110,13 @@ def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
         self.loc = loc
 
     def __repr__(self):
+        """Programming / official string representation."""
         return f"CONFIG:{self.config_option}"
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         return rf"\text{{config.{self.config_option}}}"
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         config_val = (
             self.eval_attrs["input_data"].attrs["config"].build[self.config_option]
         )
@@ -126,9 +131,10 @@ def as_array(self) -> xr.DataArray:
 
 
 class DataVarParser(EvalWhere):
+    """Data variable processing."""
+
     def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed model data variable names.
+        """Parse action to process successfully parsed model data variable names.
 
         Args:
             instring (str): String that was parsed (used in error message).
@@ -143,6 +149,7 @@ def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
         self.loc = loc
 
     def __repr__(self):
+        """Programming / official string representation."""
         return f"DATA_VAR:{self.data_var}"
 
     def _preprocess(self) -> tuple[xr.Dataset, str]:
@@ -182,7 +189,7 @@ def _preprocess(self) -> tuple[xr.Dataset, str]:
     def _data_var_exists(
         self, source_dataset: xr.Dataset, data_var_type: str
     ) -> xr.DataArray:
-        "mask by setting all (NaN | INF/-INF) to False, otherwise True"
+        """Mask by setting all (NaN | INF/-INF) to False, otherwise True."""
         var = source_dataset.get(self.data_var, xr.DataArray(np.nan))
         if data_var_type == "parameters":
             if self.data_var not in self.eval_attrs["input_data"]:
@@ -193,14 +200,17 @@ def _data_var_exists(
             return var.notnull()
 
     def _data_var_with_default(self, source_dataset: xr.Dataset) -> xr.DataArray:
-        "Access data var and fill with default values. Return default value as an array if var does not exist"
+        """Access data var and fill with default values.
+
+        Return default value as an array if var does not exist.
+        """
         default = source_dataset.attrs["defaults"].get(self.data_var)
         var = source_dataset.get(self.data_var, xr.DataArray(default))
         if default is not None:
             var = var.fillna(default)
         return var
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         # TODO: add dims from a YAML schema of params that includes default dims
         source_dataset, data_var_type = self._preprocess()
         if data_var_type == "parameters":
@@ -217,7 +227,7 @@ def as_math_string(self) -> str:
             data_var_string = rf"\exists ({data_var_string})"
         return data_var_string
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         source_dataset, data_var_type = self._preprocess()
 
         if self.eval_attrs.get("apply_where", True):
@@ -227,7 +237,8 @@ def as_array(self) -> xr.DataArray:
 
 
 class ComparisonParser(EvalWhere, expression_parser.EvalComparisonOp):
-    "Parse action to process successfully parsed strings of the form x=y"
+    """Parse action to process successfully parsed strings of the form x=y."""
+
     OP_TRANSLATOR = {
         "<=": r"\mathord{\leq}",
         ">=": r"\mathord{\geq}",
@@ -237,17 +248,17 @@ class ComparisonParser(EvalWhere, expression_parser.EvalComparisonOp):
     }
 
     def __repr__(self):
-        "Return string representation of the parsed grammar"
+        """Return string representation of the parsed grammar."""
         return f"{self.lhs}{self.op}{self.rhs}"
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         self.eval_attrs["apply_where"] = False
         lhs, rhs = self._eval("math_string")
         if r"\text" not in rhs:
             rhs = rf"\text{{{rhs}}}"
         return lhs + self.OP_TRANSLATOR[self.op] + rhs
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         self.eval_attrs["apply_where"] = False
         lhs, rhs = self._eval("array")
         match self.op:
@@ -265,9 +276,10 @@ def as_array(self) -> xr.DataArray:
 
 
 class SubsetParser(EvalWhere):
+    """Dimension subset parsing."""
+
     def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed dimension subsetting.
+        """Parse action to process successfully parsed dimension subsetting.
 
         Args:
             instring (str): String that was parsed (used in error message).
@@ -282,29 +294,31 @@ def __init__(self, instring: str, loc: int, tokens: pp.ParseResults) -> None:
         self.loc = loc
 
     def __repr__(self):
+        """Return string representation of the parsed grammar."""
         return f"SUBSET:{self.set_name}{self.val}"
 
     def _eval(self) -> list[str | float]:
-        "Evaluate each element of the subset list"
+        """Evaluate each element of the subset list."""
         values = [val.eval("array", **self.eval_attrs) for val in self.val]
         return [val.item() if isinstance(val, xr.DataArray) else val for val in values]
 
-    def as_math_string(self) -> str:
+    def as_math_string(self) -> str:  # noqa: D102, override
         subset = self._eval()
         set_singular = self.set_name.removesuffix("s")
         subset_string = "[" + ",".join(str(i) for i in subset) + "]"
         return rf"\text{{{set_singular}}} \in \text{{{subset_string}}}"
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         subset = self._eval()
         set_item_in_subset = self.eval_attrs["input_data"][self.set_name].isin(subset)
         return set_item_in_subset
 
 
 class BoolOperandParser(EvalWhere):
+    """Boolean operand parsing."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed boolean strings.
+        """Parse action to process successfully parsed boolean strings.
 
         Args:
             tokens (pp.ParseResults): Has one parsed element: boolean (str).
@@ -312,12 +326,13 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.val = tokens[0].lower()
 
     def __repr__(self):
+        """Programming / official string representation."""
         return f"BOOL:{self.val}"
 
-    def as_math_string(self):
+    def as_math_string(self):  # noqa: D102, override
         return self.val
 
-    def as_array(self) -> xr.DataArray:
+    def as_array(self) -> xr.DataArray:  # noqa: D102, override
         if self.val == "true":
             bool_val = xr.DataArray(np.True_)
         elif self.val == "false":
@@ -326,9 +341,11 @@ def as_array(self) -> xr.DataArray:
 
 
 class GenericStringParser(expression_parser.EvalString):
+    """Parsing of generic strings."""
+
     def __init__(self, tokens: pp.ParseResults) -> None:
-        """
-        Parse action to process successfully parsed generic strings.
+        """Parse action to process successfully parsed generic strings.
+
         This is required since we call "eval()" on all elements of the where string,
         so even arbitrary strings (used in comparison operations) need to be evaluatable.
 
@@ -338,26 +355,24 @@ def __init__(self, tokens: pp.ParseResults) -> None:
         self.val = tokens[0]
 
     def __repr__(self) -> str:
+        """Return string representation of the parsed grammar."""
         return f"STRING:{self.val}"
 
     def eval(self, *args, **eval_kwargs) -> str:
+        """Evaluation just returns the string of values."""
         return str(self.val)
 
 
 def data_var_parser(generic_identifier: pp.ParserElement) -> pp.ParserElement:
-    """
-    Parsing grammar to process model data variables which can be any valid python
-    identifier (string + "_")
+    """Process model data variables which can be any valid python identifier (string + "_").
 
     Args:
-        generic_identifier (pp.ParserElement):
-            Parser for valid python variables without leading underscore and not called "inf".
-            This parser has no parse action.
+        generic_identifier (pp.ParserElement): parser for valid python variables
+            without leading underscore and not called "inf". This parser has no parse action.
 
     Returns:
-        pp.ParserElement:
-            Parser for model data variables which will access the data variable from the
-            Calliope model dataset.
+        pp.ParserElement: parser for model data variables which will access the data
+            variable from the Calliope model dataset.
     """
     protected_strings = (
         pp.Keyword("and", caseless=True)
@@ -373,8 +388,7 @@ def data_var_parser(generic_identifier: pp.ParserElement) -> pp.ParserElement:
 
 
 def config_option_parser(generic_identifier: pp.ParserElement) -> pp.ParserElement:
-    """
-    Parsing grammar to process model configuration option key names of the form "x.y.z".
+    """Parsing grammar to process model configuration option key names of the form "x.y.z".
 
     Args:
         generic_identifier (pp.ParserElement):
@@ -393,7 +407,7 @@ def config_option_parser(generic_identifier: pp.ParserElement) -> pp.ParserEleme
 
 
 def bool_parser() -> pp.ParserElement:
-    "Parsing grammar for True/False (any case), which will evaluate to np.bool_"
+    """Parsing grammar for True/False (any case), which will evaluate to np.bool_."""
     TRUE = pp.Keyword("True", caseless=True)
     FALSE = pp.Keyword("False", caseless=True)
     bool_operand = TRUE | FALSE
@@ -403,7 +417,7 @@ def bool_parser() -> pp.ParserElement:
 
 
 def evaluatable_string_parser(generic_identifier: pp.ParserElement) -> pp.ParserElement:
-    "Parsing grammar to make generic strings used in comparison operations evaluatable"
+    """Parsing grammar to make generic strings used in comparison operations evaluatable."""
     evaluatable_identifier = generic_identifier.copy()
     evaluatable_identifier.set_parse_action(GenericStringParser)
 
@@ -418,16 +432,15 @@ def comparison_parser(
     config_option: pp.ParserElement,
     data_var: pp.ParserElement,
 ) -> pp.ParserElement:
-    """Parsing grammar to process comparisons of the form "variable_or_config=comparator"
+    """Parsing grammar to process comparisons of the form `variable_or_config=comparator`.
 
     Args:
         evaluatable_identifier (pp.ParserElement): parser for evaluatable generic strings
-        number (pp.ParserElement):
-            Parser for numbers (integer, float, scientific notation, "inf"/".inf")
-        bool_operand (pp.ParserElement): Parser for boolean strings
-        config_option (pp.ParserElement):
-            Parser for attribute dictionary keys of the form "x.y.z"
-        data_var (pp.ParserElement): Parser for Calliope model dataset variable names.
+        number (pp.ParserElement): parser for numbers (integer, float, scientific notation, "inf"/".inf")
+        helper_function (pp.ParserElement): parser for helper functions
+        bool_operand (pp.ParserElement): parser for boolean strings
+        config_option (pp.ParserElement): parser for attribute dictionary keys of the form "x.y.z"
+        data_var (pp.ParserElement): parser for Calliope model dataset variable names
 
     Returns:
         pp.ParserElement:
@@ -449,20 +462,15 @@ def subset_parser(
     evaluatable_identifier: pp.ParserElement,
     number: pp.ParserElement,
 ) -> pp.ParserElement:
-    """Parsing grammar to process comparisons of the form "variable_or_config=comparator"
+    """Parsing grammar to process subsets.
 
     Args:
-        evaluatable_identifier (pp.ParserElement): parser for evaluatable generic strings
-        number (pp.ParserElement):
-            Parser for numbers (integer, float, scientific notation, "inf"/".inf")
-        bool_operand (pp.ParserElement): Parser for boolean strings
-        config_option (pp.ParserElement):
-            Parser for attribute dictionary keys of the form "x.y.z"
-        data_var (pp.ParserElement): Parser for Calliope model dataset variable names.
+        generic_identifier (pp.ParserElement): generic identifier parser
+        evaluatable_identifier (pp.ParserElement): evaluatable identifier parser.
+        number (pp.ParserElement): number parser.
 
     Returns:
-        pp.ParserElement:
-            Parser which will return a bool/boolean array as a result of the comparison.
+        pp.ParserElement: subset parser.
     """
     subset = pp.Group(pp.delimited_list(number | evaluatable_identifier))
     subset_expression = (
@@ -484,19 +492,17 @@ def where_parser(
     comparison_parser: pp.ParserElement,
     subset: pp.ParserElement,
 ) -> pp.ParserElement:
-    """
-    Parsing grammar to combine bools/boolean arrays using (case agnostic) AND/OR operators
-    and optional (case agnostic) NOT (to invert the bools).
+    """Parser for strings which use AND/OR/NOT operators to combine other parser elements.
 
     Args:
-        helper_function (pp.ParserElement):
-            Parsing grammar to process helper functions of the form `helper_function(*args, **kwargs)`.
-        data_var (pp.ParserElement): Parser for Calliope model dataset variable names.
-        comparison_parser (pp.ParserElement): Parser for comparisons of the form "variable_or_config=comparator".
+        bool_operand (pp.ParserElement): parser for boolean operands.
+        helper_function (pp.ParserElement): parser for helper functions.
+        data_var (pp.ParserElement): parser for dataset variable names.
+        comparison_parser (pp.ParserElement): parser for comparisons.
+        subset (pp.ParserElement): parser for subsets.
 
     Returns:
-        pp.ParserElement:
-            Parser for strings which use AND/OR/NOT operators to combine other parser elements.
+        pp.ParserElement: where parser.
     """
     notop = pp.Keyword("not", caseless=True)
     andorop = pp.Keyword("and", caseless=True) | pp.Keyword("or", caseless=True)
@@ -513,7 +519,8 @@ def where_parser(
 
 
 def generate_where_string_parser() -> pp.ParserElement:
-    """
+    """Creates and executes the where parser.
+
     Args:
         parse_string (str): Constraint subsetting "where" string.
 
diff --git a/src/calliope/cli.py b/src/calliope/cli.py
index 18ce3ffd..c83b2b31 100644
--- a/src/calliope/cli.py
+++ b/src/calliope/cli.py
@@ -1,14 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-
-cli.py
-~~~~~~
-
-Command-line interface.
-
-"""
+"""Command-line interface."""
 
 import contextlib
 import datetime
@@ -75,6 +67,7 @@
 def format_exceptions(
     debug=False, pdb=False, profile=False, profile_filename=None, start_time=None
 ):
+    """Exception formatting for better reading."""
     try:
         if profile:
             import cProfile
@@ -113,12 +106,14 @@ def format_exceptions(
 
 
 def print_end_time(start_time, msg="complete"):
+    """Track calliope run time."""
     end_time = datetime.datetime.now()
     secs = round((end_time - start_time).total_seconds(), 1)
     tend = end_time.strftime(_time_format)
     click.secho(
-        "\nCalliope run {}. "
-        "Elapsed: {} seconds (time at exit: {})".format(msg, secs, tend)
+        "\nCalliope run {}. " "Elapsed: {} seconds (time at exit: {})".format(
+            msg, secs, tend
+        )
     )
 
 
@@ -127,12 +122,10 @@ def _get_version():
 
 
 def _cli_start(debug, quiet):
-    """
-    Initial setup for CLI commands.
-    Returns ``start_time`` (datetime timestamp)
+    """Initial setup for CLI commands.
 
+    Returns ``start_time`` (datetime timestamp).
     """
-
     if debug:
         click.secho(_get_version())
 
@@ -160,7 +153,7 @@ def _cli_start(debug, quiet):
 @click.pass_context
 @click.option("--version", is_flag=True, default=False, help="Display version.")
 def cli(ctx, version):
-    """Calliope: a multi-scale energy systems modelling framework"""
+    """Calliope: a multi-scale energy systems modelling framework."""
     if ctx.invoked_subcommand is None and not version:
         click.secho(ctx.get_help())
     if version:
@@ -172,10 +165,10 @@ def cli(ctx, version):
 @click.option("--template", type=str, default=None)
 @_debug
 def new(path, template, debug):
-    """
-    Create new model at the given ``path``, based on one of the built-in
-    example models. The target path must not yet exist. Intermediate
-    directories will be created automatically.
+    """Create new model at the given ``path`` based on one of the built-in example models.
+
+    The target path must not yet exist.
+    Intermediate directories will be created automatically.
     """
     _cli_start(debug, quiet=False)
 
@@ -188,10 +181,9 @@ def new(path, template, debug):
 
 
 def _run_setup_model(model_file, scenario, model_format, override_dict):
-    """
-    Build model in CLI commands. Returns ``model``, a ready-to-run
-    calliope.Model instance.
+    """Build model in CLI commands.
 
+    Returns ``model``, a ready-to-run calliope.Model instance.
     """
     # Try to determine model file type if not given explicitly
     if model_format is None:
@@ -256,12 +248,12 @@ def run(
     profile_filename,
     fail_when_infeasible,
 ):
-    """
-    Execute the given model. Tries to guess from the file extension whether
+    """Execute the given model.
+
+    Tries to guess from the file extension whether
     ``model_file`` is a YAML file or a pre-built model saved to NetCDF.
     This can also explicitly be set with the --model_format=yaml or
     --model_format=netcdf option.
-
     """
     start_time = _cli_start(debug, quiet)
     click.secho(
@@ -361,6 +353,7 @@ def generate_runs(
     quiet,
     pdb,
 ):
+    """Generates a script to run multiple models."""
     _cli_start(debug, quiet)
 
     kwargs = dict(
@@ -392,6 +385,7 @@ def generate_runs(
 def generate_scenarios(
     model_file, out_file, overrides, scenario_name_prefix, debug, quiet, pdb
 ):
+    """Generate scenario definitions from given combinations of overrides."""
     _cli_start(debug, quiet)
 
     with format_exceptions(debug, pdb):
diff --git a/src/calliope/examples.py b/src/calliope/examples.py
index 7a057d09..3ac4f318 100644
--- a/src/calliope/examples.py
+++ b/src/calliope/examples.py
@@ -1,9 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-Example models that can be loaded directly into a session.
-"""
+"""Example models that can be loaded directly into a session."""
 
 import importlib
 from pathlib import Path
diff --git a/src/calliope/exceptions.py b/src/calliope/exceptions.py
index 035bfa18..f9813ad9 100644
--- a/src/calliope/exceptions.py
+++ b/src/calliope/exceptions.py
@@ -1,9 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-Exceptions and Warning handling.
-"""
+"""Exceptions and Warning handling."""
 
 import textwrap
 import warnings
@@ -11,34 +8,31 @@
 
 
 class ModelError(Exception):
-    """
-    ModelErrors should stop execution of the model, e.g. due to a problem
-    with the model formulation or input data.
-
-    """
+    """Should be raised due to problems with the model formulation or input data."""
 
     pass
 
 
 class BackendError(Exception):
+    """Should be raised due to issues during backend processing."""
+
     pass
 
 
 class ModelWarning(Warning):
-    """
-    ModelWarnings should be raised for possible model errors, but
-    where execution can still continue.
-
-    """
+    """Should be raised for possible model errors, but where execution can continue."""
 
     pass
 
 
 class BackendWarning(Warning):
+    """Should be raised for possible backend processing issues where execution may continue."""
+
     pass
 
 
 def warn(message: str, _class: type[Warning] = ModelWarning):
+    """Raises the specified type of warning."""
     warnings.formatwarning = (
         lambda message, category, *args, **kwargs: f"{category.__name__}: {message}\n"
     )
@@ -52,8 +46,10 @@ def print_warnings_and_raise_errors(
     during: str = "model processing",
     bullet: str = " * ",
 ) -> None:
-    """
-    Concatenate collections of warnings/errors and print (warnings) / raise ModelError (errors) with a bullet point list of the concatenated collections.
+    """Process collections of warnings/errors.
+
+    Prints warnings / raises errors with a bullet point list of the concatenated
+    collections.
 
     Lists will return simple bullet lists:
     E.g. warnings=["foo", "bar"] becomes:
diff --git a/src/calliope/io.py b/src/calliope/io.py
index 006168fb..8c2c4584 100644
--- a/src/calliope/io.py
+++ b/src/calliope/io.py
@@ -1,13 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-io.py
-~~~~~
-
-Functions to read and save model results.
-
-"""
+"""Functions to read and save model results."""
 
 import importlib.resources
 from pathlib import Path
@@ -27,7 +20,7 @@
 
 
 def read_netcdf(path):
-    """Read model_data from NetCDF file"""
+    """Read model_data from NetCDF file."""
     with xr.open_dataset(path) as model_data:
         model_data.load()
 
@@ -119,6 +112,7 @@ def _deserialise(attrs: dict) -> None:
 
 
 def save_netcdf(model_data, path, model=None):
+    """Save the model to a netCDF file."""
     original_model_data_attrs = model_data.attrs
     model_data_attrs = original_model_data_attrs.copy()
 
@@ -155,8 +149,7 @@ def save_csv(
     dropna: bool = True,
     allow_overwrite: bool = False,
 ):
-    """
-    Save results to CSV.
+    """Save results to CSV.
 
     One file per dataset array will be generated, with the filename matching the array name.
 
@@ -203,6 +196,7 @@ def save_csv(
 
 
 def load_config(filename: str):
+    """Load model configuration from a file."""
     with importlib.resources.as_file(CONFIG_DIR / filename) as f:
         loaded = AttrDict.from_yaml(f)
     return loaded
diff --git a/src/calliope/model.py b/src/calliope/model.py
index 82aa404c..d30c384d 100644
--- a/src/calliope/model.py
+++ b/src/calliope/model.py
@@ -1,11 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-# model.py
-
-Implements the core Model class.
-"""
+"""Implements the core Model class."""
 
 from __future__ import annotations
 
@@ -46,17 +41,13 @@
 
 
 def read_netcdf(path):
-    """
-    Return a Model object reconstructed from model data in a NetCDF file.
-    """
+    """Return a Model object reconstructed from model data in a NetCDF file."""
     model_data = io.read_netcdf(path)
     return Model(model_definition=model_data)
 
 
 class Model(object):
-    """
-    A Calliope Model.
-    """
+    """A Calliope Model."""
 
     _BACKENDS: dict[str, Callable] = {"pyomo": PyomoBackendModel}
     _TS_OFFSET = pd.Timedelta(nanoseconds=1)
@@ -69,9 +60,7 @@ def __init__(
         data_source_dfs: Optional[dict[str, pd.DataFrame]] = None,
         **kwargs,
     ):
-        """
-        Returns a new Model from either the path to a YAML model
-        configuration file or a dict fully specifying the model.
+        """Returns a new Model from YAML model configuration files or a fully specified dictionary.
 
         Args:
             model_definition (str | Path | dict | xr.Dataset):
@@ -87,6 +76,7 @@ def __init__(
                 Model definition `data_source` entries can reference in-memory pandas DataFrames.
                 The referenced data must be supplied here as a dictionary of those DataFrames.
                 Defaults to None.
+            **kwargs: initialisation overrides.
         """
         self._timings: dict = {}
         self.config: AttrDict
@@ -128,22 +118,27 @@ def __init__(
 
     @property
     def name(self):
+        """Get the model name."""
         return self._model_data.attrs["name"]
 
     @property
     def inputs(self):
+        """Get model input data."""
         return self._model_data.filter_by_attrs(is_result=0)
 
     @property
     def results(self):
+        """Get model result data."""
         return self._model_data.filter_by_attrs(is_result=1)
 
     @property
     def is_built(self):
+        """Get built status."""
         return self._is_built
 
     @property
     def is_solved(self):
+        """Get solved status."""
         return self._is_solved
 
     def _init_from_model_def_dict(
@@ -233,8 +228,8 @@ def _init_from_model_def_dict(
         )
 
     def _init_from_model_data(self, model_data: xr.Dataset) -> None:
-        """
-        Initialise the model using a pre-built xarray dataset.
+        """Initialise the model using a pre-built xarray dataset.
+
         This must be a Calliope-compatible dataset, usually a dataset from another Calliope model.
 
         Args:
@@ -261,7 +256,8 @@ def _init_from_model_data(self, model_data: xr.Dataset) -> None:
         )
 
     def _add_model_data_methods(self):
-        """
+        """Add observed data to `model`.
+
         1. Filter model dataset to produce views on the input/results data
         2. Add top-level configuration dictionaries simultaneously to the model data attributes and as attributes of this class.
 
@@ -270,14 +266,16 @@ def _add_model_data_methods(self):
         self._add_observed_dict("math")
 
     def _add_observed_dict(self, name: str, dict_to_add: Optional[dict] = None) -> None:
-        """
-        Add the same dictionary as property of model object and an attribute of the model xarray dataset.
+        """Add the same dictionary as property of model object and an attribute of the model xarray dataset.
 
         Args:
             name (str):
-                Name of dictionary which will be set as the model property name and (if necessary) the dataset attribute name.
+                Name of dictionary which will be set as the model property name and
+                (if necessary) the dataset attribute name.
             dict_to_add (Optional[dict], optional):
-                If given, set as both the model property and the dataset attribute, otherwise set an existing dataset attribute as a model property of the same name. Defaults to None.
+                If given, set as both the model property and the dataset attribute,
+                otherwise set an existing dataset attribute as a model property of the
+                same name. Defaults to None.
 
         Raises:
             exceptions.ModelError: If `dict_to_add` is not given, it must be an attribute of model data.
@@ -300,8 +298,7 @@ def _add_observed_dict(self, name: str, dict_to_add: Optional[dict] = None) -> N
         setattr(self, name, dict_to_add)
 
     def _add_math(self, add_math: list) -> AttrDict:
-        """
-        Load the base math and optionally override with additional math from a list of references to math files.
+        """Load the base math and optionally override with additional math from a list of references to math files.
 
         Args:
             add_math (list):
@@ -346,8 +343,8 @@ def build(self, force: bool = False, **kwargs) -> None:
             force (bool, optional):
                 If ``force`` is True, any existing results will be overwritten.
                 Defaults to False.
+            **kwargs: build configuration overrides.
         """
-
         if self._is_built and not force:
             raise exceptions.ModelError(
                 "This model object already has a built optimisation problem. Use model.build(force=True) "
@@ -387,8 +384,7 @@ def build(self, force: bool = False, **kwargs) -> None:
         self._is_built = True
 
     def solve(self, force: bool = False, warmstart: bool = False, **kwargs) -> None:
-        """
-        Solve the built optimisation problem.
+        """Solve the built optimisation problem.
 
         Args:
             force (bool, optional):
@@ -402,13 +398,13 @@ def solve(self, force: bool = False, warmstart: bool = False, **kwargs) -> None:
                 decrease the solution time.
                 Warmstart will not work with some solvers (e.g., CBC, GLPK).
                 Defaults to False.
+            **kwargs: solve configuration overrides.
 
         Raises:
             exceptions.ModelError: Optimisation problem must already be built.
             exceptions.ModelError: Cannot run the model if there are already results loaded, unless `force` is True.
             exceptions.ModelError: Some preprocessing steps will stop a run mode of "operate" from being possible.
         """
-
         # Check that results exist and are non-empty
         if not self._is_built:
             raise exceptions.ModelError(
@@ -492,12 +488,11 @@ def solve(self, force: bool = False, warmstart: bool = False, **kwargs) -> None:
         self._is_solved = True
 
     def run(self, force_rerun=False, **kwargs):
-        """
-        Run the model. If ``force_rerun`` is True, any existing results
-        will be overwritten.
+        """Run the model.
 
-        Additional kwargs are passed to the backend.
+        If ``force_rerun`` is True, any existing results will be overwritten.
 
+        Additional kwargs are passed to the backend.
         """
         exceptions.warn(
             "`run()` is deprecated and will be removed in a "
@@ -508,22 +503,16 @@ def run(self, force_rerun=False, **kwargs):
         self.solve(force=force_rerun)
 
     def to_netcdf(self, path):
-        """
-        Save complete model data (inputs and, if available, results)
-        to a NetCDF file at the given ``path``.
-
-        """
+        """Save complete model data (inputs and, if available, results) to a NetCDF file at the given `path`."""
         io.save_netcdf(self._model_data, path, model=self)
 
     def to_csv(
         self, path: str | Path, dropna: bool = True, allow_overwrite: bool = False
     ):
-        """
-        Save complete model data (inputs and, if available, results)
-        as a set of CSV files to the given ``path``.
+        """Save complete model data (inputs and, if available, results) as a set of CSV files to the given ``path``.
 
         Args:
-            path (str | Path):
+            path (str | Path): file path to save at.
             dropna (bool, optional):
                 If True, NaN values are dropped when saving, resulting in significantly smaller CSV files.
                 Defaults to True
@@ -610,12 +599,12 @@ def _prepare_operate_mode_inputs(
     ) -> xr.Dataset:
         """Slice the input data to just the length of operate mode time horizon.
 
-
         Args:
             start_window_idx (int, optional):
                 Set the operate `window` to start at, based on integer index.
                 This is used when re-initialising the backend model for shorter time horizons close to the end of the model period.
                 Defaults to 0.
+            **config_kwargs: kwargs related to operate mode configuration.
 
         Returns:
             xr.Dataset: Slice of input data.
@@ -724,8 +713,9 @@ def _solve_operate(self, **solver_config) -> xr.Dataset:
         return results
 
     def _recalculate_storage_initial(self, results: xr.Dataset) -> xr.DataArray:
-        """Calculate the initial level of storage devices for a new operate mode time slice
-        based on storage levels at the end of the previous time slice.
+        """Calculate the initial level of storage devices for a new operate mode time slice.
+
+        Based on storage levels at the end of the previous time slice.
 
         Args:
             results (xr.Dataset): Results from the previous time slice.
diff --git a/src/calliope/postprocess/__init__.py b/src/calliope/postprocess/__init__.py
index e69de29b..5e4007cf 100644
--- a/src/calliope/postprocess/__init__.py
+++ b/src/calliope/postprocess/__init__.py
@@ -0,0 +1,1 @@
+"""Calliope's postprocessing module."""
diff --git a/src/calliope/postprocess/postprocess.py b/src/calliope/postprocess/postprocess.py
index 2bc938d4..2167e335 100644
--- a/src/calliope/postprocess/postprocess.py
+++ b/src/calliope/postprocess/postprocess.py
@@ -1,13 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-postprocess.py
-~~~~~~~~~~~~~~
-
-Functionality to post-process model results.
-
-"""
+"""Functionality to post-process model results."""
 
 import logging
 
@@ -20,22 +13,19 @@
 def postprocess_model_results(
     results: xr.Dataset, model_data: xr.Dataset
 ) -> xr.Dataset:
-    """
-    Adds additional post-processed result variables to
-    the given model results in-place. Model must have solved successfully.
+    """Post-processing of model results.
+
+    Adds additional post-processed result variables to the given model results
+    in-place. Model must have solved successfully.
+    All instances of unreasonably low numbers (set by zero_threshold) will be removed.
 
     Args:
-        results (xarray.Dataset):
-            Output from the solver backend.
-        model_data (xarray.Dataset):
-            Calliope model data, stored as [calliope.Model._model_data][].
+        results (xarray.Dataset): Output from the solver backend.
+        model_data (xarray.Dataset): Calliope model data.
 
     Returns:
-        xarray.Dataset:
-            Input results Dataset, with additional DataArray variables and all instances of unreasonably low numbers (set by zero_threshold) removed.
-
+        xarray.Dataset: input-results dataset.
     """
-
     zero_threshold = model_data.config.solve.zero_threshold
     results["capacity_factor"] = capacity_factor(results, model_data)
     results["systemwide_capacity_factor"] = capacity_factor(
@@ -58,19 +48,14 @@ def postprocess_model_results(
 
 
 def capacity_factor(results, model_data, systemwide=False):
-    """
-    # In operate mode, flow_cap is an input parameter
-    if "flow_cap" not in results.keys():
-        flow_cap = model_data.flow_cap
-    else:
-        flow_cap = results.flow_cap
+    """Calculation of capacity factors.
 
-    capacity_factors = (results["flow_out"] / flow_cap).fillna(0)
+    Processes whether `flow_cap` is a parameter or a result, then calculates the
+    capacity factor.
 
     The weight of timesteps is considered when computing systemwide capacity factors,
-    such that higher-weighted timesteps have a stronger influence
-    on the resulting system-wide time-averaged capacity factor.
-
+    such that higher-weighted timesteps have a stronger influence on the resulting
+    system-wide time-averaged capacity factor.
     """
     # In operate mode, flow_cap is an input parameter
     if "flow_cap" not in results.keys():
@@ -98,7 +83,8 @@ def capacity_factor(results, model_data, systemwide=False):
 def systemwide_levelised_cost(
     results: xr.Dataset, model_data: xr.Dataset, total: bool = False
 ) -> xr.DataArray:
-    """
+    """Calculates systemwide levelised costs.
+
     Returns a DataArray with systemwide levelised costs for the given
     results, indexed by techs, carriers and costs if total is False,
     or by carriers and costs if total is True.
@@ -119,6 +105,7 @@ def systemwide_levelised_cost(
         total (bool, optional):
             If False (default) returns per-technology levelised cost, if True,
             returns overall system-wide levelised cost.
+
     Returns:
         xr.DataArray: Array of levelised costs.
     """
@@ -148,12 +135,11 @@ def systemwide_levelised_cost(
 
 
 def clean_results(results, zero_threshold):
-    """
-    Remove unreasonably small values (solver output can lead to floating point
-    errors) and remove unmet_demand if it was never used (i.e. sum = zero)
+    """Remove unreasonably small values and unmet_demand if it was never used.
 
+    Used to avoid floating point errors caused by solver output.
     zero_threshold is a value set in model configuration. If not set, defaults
-    to zero (i.e. doesn't do anything). Reasonable value = 1e-12
+    to zero (i.e. doesn't do anything). Reasonable value = 1e-12.
     """
     threshold_applied = []
     for k, v in results.data_vars.items():
diff --git a/src/calliope/preprocess/__init__.py b/src/calliope/preprocess/__init__.py
index 8b137891..0ba2ad52 100644
--- a/src/calliope/preprocess/__init__.py
+++ b/src/calliope/preprocess/__init__.py
@@ -1,1 +1,1 @@
-
+"""Preprocessing module."""
diff --git a/src/calliope/preprocess/data_sources.py b/src/calliope/preprocess/data_sources.py
index 2df00e06..70408631 100644
--- a/src/calliope/preprocess/data_sources.py
+++ b/src/calliope/preprocess/data_sources.py
@@ -1,5 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
+"""Preprocessing functionality."""
 
 import logging
 from pathlib import Path
@@ -27,6 +28,8 @@
 
 
 class DataSourceDict(TypedDict):
+    """Uniform dictionary for data sources."""
+
     rows: NotRequired[str | list[str]]
     columns: NotRequired[str | list[str]]
     source: str
@@ -37,6 +40,8 @@ class DataSourceDict(TypedDict):
 
 
 class DataSource:
+    """Class for in memory data handling."""
+
     MESSAGE_TEMPLATE = "(data_sources, {name}) | {message}."
     PARAMS_TO_INITIALISE_YAML = ["base_tech", "to", "from"]
 
@@ -52,6 +57,7 @@ def __init__(
 
         Args:
             model_config (dict): Model initialisation configuration dictionary.
+            source_name (str): name of the data source.
             data_source (DataSourceDict): Data source definition dictionary.
             data_source_dfs (Optional[dict[str, pd.DataFrame]]):
                 If given, a dictionary mapping source names in `data_source` to in-memory pandas DataFrames.
@@ -80,7 +86,7 @@ def __init__(
 
     @property
     def name(self):
-        "Data source name"
+        """Data source name."""
         return self._name
 
     def drop(self, name: str):
@@ -99,7 +105,6 @@ def tech_dict(self) -> tuple[AttrDict, AttrDict]:
         This function should be accessed _before_ `self.node_dict`.
 
         """
-
         tech_dict = AttrDict(
             {k: {} for k in self.dataset.get("techs", xr.DataArray([])).values}
         )
@@ -123,7 +128,6 @@ def node_dict(self, techs_incl_inheritance: AttrDict) -> AttrDict:
                 Technology definition dictionary which is a union of any YAML definition and the result of calling `self.tech_dict` across all data sources.
                 Technologies should have their entire definition inheritance chain resolved.
         """
-
         node_tech_vars = self.dataset[
             [
                 k
@@ -190,6 +194,7 @@ def lookup_dict_from_param(self, param: str, lookup_dim: str) -> AttrDict:
             lookup_dim (str):
                 The dimension to pivot the data table on.
                 The values in this dimension will become the values in the dictionary.
+
         Returns:
             AttrDict:
                 If present in `self.dataset`, a valid Calliope YAML format for the parameter.
@@ -351,13 +356,13 @@ def _check_processed_tdf(self, tdf: pd.Series):
             self._raise_error(f"Duplicate dimension names found: {tdf.index.names}")
 
     def _raise_error(self, message):
-        "Format error message and then raise Calliope ModelError."
+        """Format error message and then raise Calliope ModelError."""
         raise exceptions.ModelError(
             self.MESSAGE_TEMPLATE.format(name=self.name, message=message)
         )
 
     def _log(self, message, level="debug"):
-        "Format log message and then log to `level`."
+        """Format log message and then log to `level`."""
         getattr(LOGGER, level)(
             self.MESSAGE_TEMPLATE.format(name=self.name, message=message)
         )
diff --git a/src/calliope/preprocess/load.py b/src/calliope/preprocess/load.py
index 11bc5aad..ba3f21fa 100644
--- a/src/calliope/preprocess/load.py
+++ b/src/calliope/preprocess/load.py
@@ -1,13 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-model_run.py
-~~~~~~~~~~~~
-
-Preprocessing of base model definition and overrides/scenarios into a unified dictionary
-
-"""
+"""Preprocessing of base model definition and overrides/scenarios into a unified dictionary."""
 
 import logging
 from pathlib import Path
@@ -26,8 +19,7 @@ def load_model_definition(
     override_dict: Optional[dict] = None,
     **kwargs,
 ) -> tuple[AttrDict, Optional[Path], str]:
-    """
-    Load model definition from file / dictionary and apply user-defined overrides.
+    """Load model definition from file / dictionary and apply user-defined overrides.
 
     Args:
         model_definition (str | Path | dict):
@@ -42,12 +34,13 @@ def load_model_definition(
             If not None, dictionary of overrides to apply.
             These will be applied _after_ `scenario` overrides.
             Defaults to None.
+        **kwargs: initialisation overrides.
 
     Returns:
         tuple[AttrDict, Optional[Path], str]:
             1. Model definition with overrides applied.
-            1. Path to model definition YAML if input `model_definiton` was pathlike, otherwise None.
-            1. Expansion of scenarios (which are references to model overrides) into a list of named override(s) that have been applied.
+            2. Path to model definition YAML if input `model_definiton` was pathlike, otherwise None.
+            3. Expansion of scenarios (which are references to model overrides) into a list of named override(s) that have been applied.
     """
     if not isinstance(model_definition, dict):
         model_def_path = Path(model_definition)
@@ -80,8 +73,10 @@ def _combine_overrides(overrides: AttrDict, scenario_overrides: list):
             combined_override_dict.union(override_with_imports, allow_override=False)
         except KeyError as e:
             raise exceptions.ModelError(
-                str(e)[1:-1] + ". Already specified but defined again in "
-                "override `{}`.".format(override)
+                str(e)[1:-1]
+                + ". Already specified but defined again in " "override `{}`.".format(
+                    override
+                )
             )
 
     return combined_override_dict
@@ -92,8 +87,7 @@ def _apply_overrides(
     scenario: Optional[str] = None,
     override_dict: Optional[str | dict] = None,
 ) -> tuple[AttrDict, list[str]]:
-    """
-    Generate processed Model configuration, applying any scenario overrides.
+    """Generate processed Model configuration, applying any scenario overrides.
 
     Args:
         model_def (calliope.Attrdict): Loaded model definition as an attribute dictionary.
@@ -112,7 +106,6 @@ def _apply_overrides(
             1. Model definition dictionary with overrides applied from `scenario` and `override_dict`.
             1. Expansion of scenarios (which are references to model overrides) into a list of named override(s) that have been applied.
     """
-
     # The input files are allowed to override other model defaults
     model_def_copy = model_def.copy()
 
diff --git a/src/calliope/preprocess/model_data.py b/src/calliope/preprocess/model_data.py
index 85390f34..88f3c2cf 100644
--- a/src/calliope/preprocess/model_data.py
+++ b/src/calliope/preprocess/model_data.py
@@ -1,5 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
+"""Model data processing functionality."""
 
 import itertools
 import logging
@@ -24,12 +25,16 @@
 
 
 class Param(TypedDict):
+    """Uniform dictionairy for parameters."""
+
     data: DATA_T
     index: list[list[str]]
     dims: list[str]
 
 
 class ModelDefinition(TypedDict):
+    """Uniform dictionarity for model definition."""
+
     techs: AttrDict
     nodes: AttrDict
     tech_groups: NotRequired[AttrDict]
@@ -41,6 +46,8 @@ class ModelDefinition(TypedDict):
 
 
 class ModelDataFactory:
+    """Model data production class."""
+
     # TODO: move into yaml syntax and have it be updatable
     LOOKUP_PARAMS = {
         "carrier_in": "carriers",
@@ -70,8 +77,7 @@ def __init__(
         attributes: dict,
         param_attributes: dict[str, dict],
     ):
-        """Take a Calliope model definition dictionary and convert it into an xarray Dataset, ready for
-        constraint generation.
+        """Take a Calliope model definition dictionary and convert it into an xarray Dataset, ready for constraint generation.
 
         This includes resampling/clustering timeseries data as necessary.
 
@@ -82,7 +88,6 @@ def __init__(
             attributes (dict): Attributes to attach to the model Dataset.
             param_attributes (dict[str, dict]): Attributes to attach to the generated model DataArrays.
         """
-
         self.config: dict = model_config
         self.model_definition: ModelDefinition = model_definition.copy()
         self.dataset = xr.Dataset(attrs=AttrDict(attributes))
@@ -97,7 +102,7 @@ def __init__(
         self.param_attrs = flipped_attributes
 
     def build(self):
-        "Build dataset from model definition."
+        """Build dataset from model definition."""
         self.add_node_tech_data()
         self.add_top_level_params()
         self.clean_data_from_undefined_members()
@@ -458,8 +463,9 @@ def _definition_dict_to_ds(
     def _prepare_param_dict(
         self, param_name: str, param_data: dict | list[str] | DATA_T
     ) -> Param:
-        """Convert a range of parameter definitions into the blessed `Param` format, i.e.:
+        """Convert a range of parameter definitions into the `Param` format.
 
+        Format is:
         ```
         data: numeric/boolean/string data or list of them.
         index: list of lists containing dimension index items (number of items in the sub-lists == length of `dims`).
@@ -471,7 +477,8 @@ def _prepare_param_dict(
             param_data (dict | list[str] | DATA_T): Input unformatted parameter data.
 
         Raises:
-            ValueError: If the parameter is unindexed (i.e., no `dims`/`index`) and is not a lookup array (see LOOKUP_PARAMS), it cannot define a list of data.
+            ValueError: If the parameter is unindexed (i.e., no `dims`/`index`) and is
+                not a lookup array (see LOOKUP_PARAMS), it cannot define a list of data.
 
         Returns:
             Param: Blessed parameter dictionary.
@@ -515,6 +522,7 @@ def _inherit_defs(
                 Base dictionary to work from.
                 If not defined, `dim_name` will be used to access the dictionary from the base model definition.
                 Defaults to None.
+
         Keyword Args:
             connected_dims (str):
                 Any dimension index items connected to the one for which we're tracing inheritance.
@@ -600,7 +608,6 @@ def _climb_inheritance_tree(
         Returns:
             tuple[AttrDict, Optional[list]]: Definition dictionary with inherited data and a list of the inheritance tree climbed to get there.
         """
-
         to_inherit = dim_item_dict.get("inherit", None)
         dim_groups = AttrDict(
             self.model_definition.get(f"{dim_name.removesuffix('s')}_groups", {})
@@ -715,8 +722,9 @@ def _add_to_dataset(self, to_add: xr.Dataset, id_: str):
         ).fillna(self.dataset)
 
     def _log_param_updates(self, param_name: str, param_da: xr.DataArray):
-        """
-        Check array coordinates to see if:
+        """Logger for model parameter updates.
+
+        Checks array coordinates to see if:
             1. any are new compared to the base model dimensions.
             2. any are adding new elements to an existing base model dimension.
 
@@ -741,7 +749,10 @@ def _log_param_updates(self, param_name: str, param_da: xr.DataArray):
 
     @staticmethod
     def _update_one_way_links(node_from_data: dict, node_to_data: dict):
-        """For one-way transmission links, delete option to have carrier outflow (imports) at the `from` node and carrier inflow (exports) at the `to` node.
+        """Update functionality for one-way links.
+
+        For one-way transmission links, delete option to have carrier outflow (imports)
+        at the `from` node and carrier inflow (exports) at the `to` node.
 
         Deletions happen on the tech definition dictionaries in-place.
 
@@ -767,7 +778,6 @@ def _update_numeric_dims(ds: xr.Dataset, id_: str) -> xr.Dataset:
         Returns:
             xr.Dataset: Input `ds` with numeric coordinates.
         """
-
         for dim_name in ds.dims:
             try:
                 ds.coords[dim_name] = pd.to_numeric(ds.coords[dim_name].to_index())
diff --git a/src/calliope/preprocess/time.py b/src/calliope/preprocess/time.py
index de113dd7..488f16cc 100644
--- a/src/calliope/preprocess/time.py
+++ b/src/calliope/preprocess/time.py
@@ -1,13 +1,7 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
+"""Functionality to add and process time varying parameters."""
 
-"""
-time.py
-~~~~~~~
-
-Functionality to add and process time varying parameters
-
-"""
 import logging
 from pathlib import Path
 from typing import overload
@@ -23,10 +17,12 @@
 
 
 def add_inferred_time_params(model_data: xr.Dataset):
-    # Add timestep_resolution by looking at the time difference between timestep n
-    # and timestep n + 1 for all timesteps
-    # Last timestep has no n + 1, so will be NaT (not a time), we ffill this.
-    # Time resolution is saved in hours (i.e. nanoseconds / 3600e6)
+    """Add timestep_resolution.
+
+    Done by ooking at the time difference between timestep n and timestep n + 1 for all timesteps.
+    Last timestep has no n + 1, so will be NaT (not a time), we ffill this.
+    Time resolution is saved in hours (i.e. nanoseconds / 3600e6)
+    """
     timestep_resolution = (
         model_data.timesteps.diff("timesteps", label="lower")
         .reindex({"timesteps": model_data.timesteps})
@@ -82,7 +78,6 @@ def subset_timeseries(ds: xr.Dataset, time_subset: list[str]) -> xr.Dataset:
     Returns:
         xr.Dataset: Input `ds` with subset timeseries coordinates.
     """
-
     datetime_dims = [k for k, v in ds.coords.items() if v.dtype.kind == "M"]
     for dim_name in datetime_dims:
         _check_time_subset(ds.coords[dim_name].to_index(), time_subset)
@@ -92,9 +87,9 @@ def subset_timeseries(ds: xr.Dataset, time_subset: list[str]) -> xr.Dataset:
 
 
 def resample(data: xr.Dataset, resolution: str) -> xr.Dataset:
-    """
-    Function to resample timeseries data from the input resolution (e.g. 1h), to
-    the given resolution (e.g. 2h)
+    """Function to resample timeseries data.
+
+    Transforms the input resolution (e.g. 1h), to the given resolution (e.g. 2h).
 
     Args:
         data (xarray.Dataset): Calliope model data, containing only timeseries data variables.
@@ -143,8 +138,7 @@ def resample(data: xr.Dataset, resolution: str) -> xr.Dataset:
 
 
 def cluster(data: xr.Dataset, clustering_file: str | Path, time_format: str):
-    """
-    Apply the given clustering time series to the given data.
+    """Apply the given clustering time series to the given data.
 
     Args:
         data (xarray.Dataset): Calliope model data, containing only timeseries data variables.
@@ -184,13 +178,11 @@ def cluster(data: xr.Dataset, clustering_file: str | Path, time_format: str):
 
 
 @overload
-def _datetime_index(index: pd.Index, format: str) -> pd.Index:
-    "Pass pandas Index"
+def _datetime_index(index: pd.Index, format: str) -> pd.Index: ...
 
 
 @overload
-def _datetime_index(index: pd.Series, format: str) -> pd.Series:
-    "Pass pandas Series"
+def _datetime_index(index: pd.Series, format: str) -> pd.Series: ...
 
 
 def _datetime_index(index: pd.Index | pd.Series, format: str) -> pd.Index | pd.Series:
@@ -264,12 +256,12 @@ def _check_missing_data(ds: xr.Dataset, dim_name: str):
 
 
 def _lookup_clusters(dataset: xr.Dataset, grouper: pd.Series) -> xr.Dataset:
-    """
+    """Clustering lookup functionality.
+
     For any given timestep in a time clustered model, get:
     1. the first and last timestep of the cluster,
     2. the last timestep of the cluster corresponding to a date in the original timeseries
     """
-
     dataset["lookup_cluster_first_timestep"] = dataset.timesteps.isin(
         dataset.timesteps.groupby("timesteps.date").first()
     )
diff --git a/src/calliope/util/__init__.py b/src/calliope/util/__init__.py
index e69de29b..1eb3e37a 100644
--- a/src/calliope/util/__init__.py
+++ b/src/calliope/util/__init__.py
@@ -0,0 +1,1 @@
+"""Util packaging."""
diff --git a/src/calliope/util/generate_runs.py b/src/calliope/util/generate_runs.py
index f4b9d41b..9665208e 100644
--- a/src/calliope/util/generate_runs.py
+++ b/src/calliope/util/generate_runs.py
@@ -1,13 +1,10 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
 
-"""
-generate_runs.py
-~~~~~~~~~~~~~~~~
-
-Generate scripts to run multiple versions of the same model
-in parallel on a cluster or sequentially on any machine.
+"""Run generation helper functions.
 
+Generate scripts to run multiple versions of the same model in parallel on a cluster
+or sequentially on any machine.
 """
 
 import os
@@ -18,8 +15,7 @@
 
 
 def generate_runs(model_file, scenarios=None, additional_args=None, override_dict=None):
-    """
-    Returns a list of "calliope run" invocations.
+    """Returns a list of "calliope run" invocations.
 
     ``scenarios`` must be specified as either a semicolon-separated
     list of scenarios or a semicolon-separated list of comma-separated
@@ -70,6 +66,7 @@ def generate_runs(model_file, scenarios=None, additional_args=None, override_dic
 def generate_bash_script(
     out_file, model_file, scenarios, additional_args=None, override_dict=None, **kwargs
 ):
+    """Produces a bash script to aid in running multiple models."""
     commands = generate_runs(model_file, scenarios, additional_args, override_dict)
 
     base_string = "    {i}) {cmd} ;;\n"
@@ -119,6 +116,7 @@ def generate_bsub_script(
     cluster_threads=1,
     **kwargs,
 ):
+    """BSUB (IBM Spectrum LSF) script generator."""
     # We also need to generate the bash script to run on the cluster
     bash_out_file = out_file + ".array.sh"
     bash_out_file_basename = os.path.basename(bash_out_file)
@@ -154,10 +152,7 @@ def generate_sbatch_script(
     cluster_threads=1,
     **kwargs,
 ):
-    """
-    SBATCH (SLURM) script generator.
-    """
-
+    """SBATCH (SLURM) script generator."""
     # We also need to generate the bash script to run on the cluster
     bash_out_file = out_file + ".array.sh"
     bash_out_file_basename = os.path.basename(bash_out_file)
@@ -206,6 +201,7 @@ def generate_sbatch_script(
 def generate_windows_script(
     out_file, model_file, scenarios, additional_args=None, override_dict=None, **kwargs
 ):
+    """Windows script generator."""
     commands = generate_runs(model_file, scenarios, additional_args, override_dict)
 
     # \r\n are Windows line endings
@@ -233,4 +229,5 @@ def generate_windows_script(
 
 
 def generate(kind, **kwargs):
+    """Generate scripts for several parallel cluster platforms."""
     _KINDS[kind](**kwargs)
diff --git a/src/calliope/util/logging.py b/src/calliope/util/logging.py
index 379b05e3..78f0c235 100644
--- a/src/calliope/util/logging.py
+++ b/src/calliope/util/logging.py
@@ -1,9 +1,7 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
 
-"""
-Create the Calliope logger object and apply other logging tools/functionality.
-"""
+"""Create the Calliope logger object and apply other logging tools/functionality."""
 
 import datetime
 import logging
@@ -69,9 +67,7 @@ def set_log_verbosity(
     include_solver_output: bool = True,
     capture_warnings: bool = True,
 ):
-    """
-    Set the verbosity of logging and setup the root logger to log to
-    console (stdout) with timestamp output formatting.
+    """Set the verbosity of logging and setup the root logger to log to console (stdout) with timestamp output formatting.
 
     Args:
         verbosity (str | int):
@@ -106,8 +102,7 @@ def log_time(
     level: str = "info",
     time_since_solve_start: bool = False,
 ) -> float:
-    """
-    Simultaneously log the time of a Calliope event to dictionary and to the logger.
+    """Simultaneously log the time of a Calliope event to dictionary and to the logger.
 
     Args:
         logger (logging.Logger): Logger to use for logging the time.
@@ -139,17 +134,21 @@ def log_time(
 
 
 class LogWriter:
+    """Log writing helper class."""
+
     def __init__(self, logger, level, strip=False):
-        "Custom logger to redirect solver outputs to avoid message duplication."
+        """Custom logger to redirect solver outputs to avoid message duplication."""
         self.logger = logger
         self.level = level
         self.strip = strip
 
     def write(self, message):
+        """Save a message to the logger."""
         if message != "\n":
             if self.strip:
                 message = message.strip()
             getattr(self.logger, self.level)(message)
 
     def flush(self):
+        """Placeholder for future flush functionality."""
         pass
diff --git a/src/calliope/util/schema.py b/src/calliope/util/schema.py
index 9770aa8e..9b13a9f6 100644
--- a/src/calliope/util/schema.py
+++ b/src/calliope/util/schema.py
@@ -1,9 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
-"""
-Load, update, and access attributes in the Calliope pre-defined YAML schemas
-"""
+"""Load, update, and access attributes in the Calliope pre-defined YAML schemas."""
 
 import importlib
 import re
@@ -31,6 +28,7 @@ def reset():
 def update_then_validate_config(
     config_key: str, config_dict: AttrDict, **update_kwargs
 ) -> AttrDict:
+    """Return an updated version of the configuration schema."""
     to_validate = deepcopy(config_dict[config_key])
     to_validate.union(AttrDict(update_kwargs), allow_override=True)
     validate_dict(
@@ -82,8 +80,7 @@ def update_model_schema(
 
 
 def validate_dict(to_validate: dict, schema: dict, dict_descriptor: str) -> None:
-    """
-    Validate a dictionary under a given schema.
+    """Validate a dictionary under a given schema.
 
     Args:
         to_validate (dict): Dictionary to validate.
diff --git a/src/calliope/util/tools.py b/src/calliope/util/tools.py
index e0d64634..82194927 100644
--- a/src/calliope/util/tools.py
+++ b/src/calliope/util/tools.py
@@ -1,6 +1,6 @@
 # Copyright (C) since 2013 Calliope contributors listed in AUTHORS.
 # Licensed under the Apache 2.0 License (see LICENSE file).
-
+"""Assorted helper tools."""
 
 from pathlib import Path
 from typing import Any, TypeVar
@@ -12,10 +12,10 @@
 
 
 def relative_path(base_path_file, path) -> Path:
-    """
+    """Path standardization.
+
     If ``path`` is not absolute, it is interpreted as relative to the
     path of the given ``base_path_file``.
-
     """
     # Check if base_path_file is a string because it might be an AttrDict
     path = Path(path)


Test patch:
diff --git a/tests/test_backend_expression_parser.py b/tests/test_backend_expression_parser.py
index a6e04440..d77f0877 100644
--- a/tests/test_backend_expression_parser.py
+++ b/tests/test_backend_expression_parser.py
@@ -35,40 +35,40 @@ def as_array(self, x, y):
         return x * 10 + y
 
 
-@pytest.fixture
+@pytest.fixture()
 def valid_component_names():
     return ["foo", "with_inf", "only_techs", "no_dims", "multi_dim_var", "no_dim_var"]
 
 
-@pytest.fixture
+@pytest.fixture()
 def base_parser_elements():
     number, identifier = expression_parser.setup_base_parser_elements()
     return number, identifier
 
 
-@pytest.fixture
+@pytest.fixture()
 def number(base_parser_elements):
     return base_parser_elements[0]
 
 
-@pytest.fixture
+@pytest.fixture()
 def identifier(base_parser_elements):
     return base_parser_elements[1]
 
 
-@pytest.fixture
+@pytest.fixture()
 def evaluatable_identifier(identifier, valid_component_names):
     return expression_parser.evaluatable_identifier_parser(
         identifier, valid_component_names
     )
 
 
-@pytest.fixture
+@pytest.fixture()
 def id_list(number, evaluatable_identifier):
     return expression_parser.list_parser(number, evaluatable_identifier)
 
 
-@pytest.fixture
+@pytest.fixture()
 def unsliced_param():
     def _unsliced_param(valid_component_names):
         return expression_parser.unsliced_object_parser(valid_component_names)
@@ -76,12 +76,12 @@ def _unsliced_param(valid_component_names):
     return _unsliced_param
 
 
-@pytest.fixture
+@pytest.fixture()
 def unsliced_param_with_obj_names(unsliced_param, valid_component_names):
     return unsliced_param(valid_component_names)
 
 
-@pytest.fixture
+@pytest.fixture()
 def sliced_param(
     number, identifier, evaluatable_identifier, unsliced_param_with_obj_names
 ):
@@ -90,12 +90,12 @@ def sliced_param(
     )
 
 
-@pytest.fixture
+@pytest.fixture()
 def sub_expression(identifier):
     return expression_parser.sub_expression_parser(identifier)
 
 
-@pytest.fixture
+@pytest.fixture()
 def helper_function(
     number,
     sliced_param,
@@ -115,7 +115,7 @@ def helper_function(
     )
 
 
-@pytest.fixture
+@pytest.fixture()
 def helper_function_no_nesting(
     number,
     sliced_param,
@@ -156,7 +156,7 @@ def helper_function_one_parser_in_args(identifier, request):
     )
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def eval_kwargs(dummy_pyomo_backend_model):
     return {
         "helper_functions": helper_functions._registry["expression"],
@@ -171,7 +171,7 @@ def eval_kwargs(dummy_pyomo_backend_model):
     }
 
 
-@pytest.fixture
+@pytest.fixture()
 def arithmetic(
     helper_function, number, sliced_param, sub_expression, unsliced_param_with_obj_names
 ):
@@ -184,7 +184,7 @@ def arithmetic(
     )
 
 
-@pytest.fixture
+@pytest.fixture()
 def helper_function_allow_arithmetic(
     number,
     sliced_param,
@@ -208,29 +208,29 @@ def helper_function_allow_arithmetic(
     )
 
 
-@pytest.fixture
+@pytest.fixture()
 def equation_comparison(arithmetic):
     return expression_parser.equation_comparison_parser(arithmetic)
 
 
-@pytest.fixture
+@pytest.fixture()
 def generate_equation(valid_component_names):
     return expression_parser.generate_equation_parser(valid_component_names)
 
 
-@pytest.fixture
+@pytest.fixture()
 def generate_slice(valid_component_names):
     return expression_parser.generate_slice_parser(valid_component_names)
 
 
-@pytest.fixture
+@pytest.fixture()
 def generate_sub_expression(valid_component_names):
     return expression_parser.generate_sub_expression_parser(valid_component_names)
 
 
 class TestEquationParserElements:
     @pytest.mark.parametrize(
-        ["string_val", "expected"],
+        ("string_val", "expected"),
         [
             ("0", 0.0),
             ("0.11", 0.11),
@@ -352,7 +352,7 @@ def test_unsliced_param_fail(self, unsliced_param_with_obj_names, string_val):
             unsliced_param_with_obj_names.parse_string(string_val, parse_all=True)
 
     @pytest.mark.parametrize(
-        ["string_val", "expected"],
+        ("string_val", "expected"),
         [
             ("foo[techs=tech]", "SLICED_COMPONENT:foo[techs=STRING:tech]"),
             (
@@ -430,7 +430,7 @@ def test_fail_sub_expression(self, sub_expression, string_val):
             sub_expression.parse_string(string_val, parse_all=True)
 
     @pytest.mark.parametrize(
-        ["string_val", "expected"],
+        ("string_val", "expected"),
         [
             (
                 f"dummy_func_1(foo[bars={SUB_EXPRESSION_CLASSIFIER}bar])",
@@ -463,7 +463,7 @@ def test_function_parse(self, helper_function, string_val, expected):
         assert parsed_[0] == expected
 
     @pytest.mark.parametrize(
-        ["string_val", "expected"],
+        ("string_val", "expected"),
         [
             ("dummy_func_1(1)", 10),
             ("dummy_func_1(x=1)", 10),
@@ -577,7 +577,7 @@ def float2(self, request):
         return float(request.param)
 
     @pytest.mark.parametrize(
-        ["sign", "sign_name"],
+        ("sign", "sign_name"),
         [("+", "add"), ("-", "sub"), ("*", "mul"), ("/", "truediv"), ("**", "pow")],
     )
     @pytest.mark.parametrize(
@@ -597,7 +597,7 @@ def test_addition_multiplication(
                 getattr(operator, sign_name)(float1, float2)
             )
 
-    @pytest.mark.parametrize(["sign", "sign_name"], [("+", "pos"), ("-", "neg")])
+    @pytest.mark.parametrize(("sign", "sign_name"), [("+", "pos"), ("-", "neg")])
     @pytest.mark.parametrize(
         "func_string", ["arithmetic", "helper_function_allow_arithmetic"]
     )
@@ -612,7 +612,7 @@ def test_sign(self, float1, sign, sign_name, eval_kwargs, func_string, request):
             assert evaluated_ == getattr(operator, sign_name)(float1)
 
     @pytest.mark.parametrize(
-        ["equation_string", "expected"],
+        ("equation_string", "expected"),
         [
             ("1+2*2", 5),
             ("-1 - 2", -3),
@@ -670,7 +670,7 @@ def test_helper_function_no_arithmetic(self, helper_function, string_):
             helper_function.parse_string(helper_func_string, parse_all=True)
 
     @pytest.mark.parametrize(
-        ["string_", "expected"], [("1 + 2", 30), ("1 * 2", 20), ("x=1/2", 5)]
+        ("string_", "expected"), [("1 + 2", 30), ("1 * 2", 20), ("x=1/2", 5)]
     )
     def test_helper_function_allow_arithmetic(
         self, helper_function_allow_arithmetic, eval_kwargs, string_, expected
@@ -757,11 +757,11 @@ def var_left(self, request):
     def var_right(self, request):
         return request.param
 
-    @pytest.fixture
+    @pytest.fixture()
     def expected_left(self, var_left):
         return self.EXPR_PARAMS_AND_EXPECTED_EVAL[var_left]
 
-    @pytest.fixture
+    @pytest.fixture()
     def expected_right(self, var_right):
         return self.EXPR_PARAMS_AND_EXPECTED_EVAL[var_right]
 
@@ -769,7 +769,7 @@ def expected_right(self, var_right):
     def operator(self, request):
         return request.param
 
-    @pytest.fixture
+    @pytest.fixture()
     def single_equation_simple(self, var_left, var_right, operator):
         return f"{var_left} {operator} {var_right}"
 
@@ -791,14 +791,12 @@ def test_simple_equation(
         assert evaluated_expression.op == operator
 
     @pytest.mark.parametrize(
-        ["equation_string", "expected"],
+        ("equation_string", "expected"),
         [
             ("1<=2", True),
             ("1 >= 2", False),
             ("1  ==  2", False),
             ("(1) <= (2)", True),
-            ("1 >= 2", False),
-            ("1 >= 2", False),
             ("1 * 3 <= 1e2", True),
             ("-1 >= -0.1 / 2", False),
             ("2**2 == 4 * 1 / 1 * 1**1", True),
@@ -850,7 +848,7 @@ def test_repr(self, equation_comparison):
 
 
 class TestAsMathString:
-    @pytest.fixture
+    @pytest.fixture()
     def latex_eval_kwargs(self, dummy_latex_backend_model):
         return {
             "helper_functions": helper_functions._registry["expression"],
@@ -865,7 +863,7 @@ def latex_eval_kwargs(self, dummy_latex_backend_model):
         }
 
     @pytest.mark.parametrize(
-        ["parser", "instring", "expected"],
+        ("parser", "instring", "expected"),
         [
             ("number", "1", "1"),
             ("number", "1.0", "1"),
diff --git a/tests/test_backend_helper_functions.py b/tests/test_backend_helper_functions.py
index 235f53a0..59541a06 100644
--- a/tests/test_backend_helper_functions.py
+++ b/tests/test_backend_helper_functions.py
@@ -71,7 +71,7 @@ def parsing_kwargs(self, dummy_model_data):
             "return_type": "array",
         }
 
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def is_defined_any(self, dummy_model_data):
         def _is_defined(drop_dims, dims):
             return (
@@ -82,7 +82,7 @@ def _is_defined(drop_dims, dims):
 
         return _is_defined
 
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def is_defined_all(self, dummy_model_data):
         def _is_defined(drop_dims, dims):
             return (
@@ -94,10 +94,10 @@ def _is_defined(drop_dims, dims):
         return _is_defined
 
     @pytest.mark.parametrize(
-        ["string_type", "func_name"], [("where", "inheritance"), ("expression", "sum")]
+        ("string_type", "func_name"), [("where", "inheritance"), ("expression", "sum")]
     )
     def test_duplicate_name_exception(self, string_type, func_name):
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError, match=rf".*{string_type}.*{func_name}.*"):
 
             class MyNewFunc(helper_functions.ParsingHelperFunction):
                 NAME = func_name
@@ -106,13 +106,8 @@ class MyNewFunc(helper_functions.ParsingHelperFunction):
                 def __call__(self):
                     return None
 
-        assert check_error_or_warning(
-            excinfo,
-            f"`{string_type}` string helper function `{func_name}` already exists",
-        )
-
     @pytest.mark.parametrize(
-        ["string_types", "func_name"],
+        ("string_types", "func_name"),
         [(["where"], "sum"), (["expression", "where"], "my_new_func")],
     )
     def test_new_func(self, string_types, func_name):
@@ -142,7 +137,7 @@ def test_any_not_exists(self, where_any):
         assert summed.equals(xr.DataArray(False))
 
     @pytest.mark.parametrize(
-        ["var", "over", "expected"],
+        ("var", "over", "expected"),
         [("with_inf", "techs", "nodes_true"), ("all_nan", "techs", "nodes_false")],
     )
     def test_any_exists(self, where_any, dummy_model_data, var, over, expected):
@@ -220,7 +215,7 @@ def test_reduce_carrier_dim(self, expression_reduce_carrier_dim, dummy_model_dat
         assert not set(reduced.dims).symmetric_difference(["nodes", "techs"])
 
     @pytest.mark.parametrize(
-        ["lookup", "expected"],
+        ("lookup", "expected"),
         [
             (
                 {"techs": "lookup_techs"},
@@ -286,7 +281,7 @@ def test_select_from_lookup_arrays_no_match(
             "Trying to select items on the dimension techs from the lookup_techs_no_match lookup array, but no matches found.",
         )
 
-    @pytest.mark.parametrize(["idx", "expected"], [(0, "foo"), (1, "bar"), (-1, "bar")])
+    @pytest.mark.parametrize(("idx", "expected"), [(0, "foo"), (1, "bar"), (-1, "bar")])
     def test_get_val_at_index(self, expression_get_val_at_index, idx, expected):
         assert expression_get_val_at_index(timesteps=idx) == expected
 
@@ -294,13 +289,13 @@ def test_get_val_at_index(self, expression_get_val_at_index, idx, expected):
     def test_get_val_at_index_not_one_mapping(
         self, expression_get_val_at_index, mapping
     ):
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError) as excinfo:  # noqa: PT011, false positive.
             expression_get_val_at_index(**mapping)
         assert check_error_or_warning(
             excinfo, "Supply one (and only one) dimension:index mapping"
         )
 
-    @pytest.mark.parametrize(["to_roll", "expected"], [(1, 3), (1.0, 3), (-3.0, 3)])
+    @pytest.mark.parametrize(("to_roll", "expected"), [(1, 3), (1.0, 3), (-3.0, 3)])
     def test_roll(self, expression_roll, dummy_model_data, to_roll, expected):
         rolled = expression_roll(dummy_model_data.with_inf, techs=to_roll)
         assert rolled.sel(nodes="foo", techs="foobar") == expected
@@ -394,7 +389,7 @@ def test_defined_all_multi_dim(self, where_defined):
         )
 
     @pytest.mark.parametrize(
-        ["over", "expected_substring"],
+        ("over", "expected_substring"),
         [
             ("techs", r"\text{tech} \in \text{techs}"),
             (["techs"], r"\substack{\text{tech} \in \text{techs}}"),
@@ -405,7 +400,7 @@ def test_defined_all_multi_dim(self, where_defined):
         ],
     )
     @pytest.mark.parametrize(
-        ["func_string", "latex_func"],
+        ("func_string", "latex_func"),
         [("where_any", r"\bigvee"), ("expression_sum", r"\sum")],
     )
     def test_sum(self, request, over, expected_substring, func_string, latex_func):
@@ -439,12 +434,11 @@ def test_get_val_at_index(self, expression_get_val_at_index):
         assert outstring == "timesteps[1]"
 
     @pytest.mark.parametrize(
-        ["instring", "expected_substring"],
+        ("instring", "expected_substring"),
         [
             (r"\textit{foo}_\text{foo}", r"foo+1"),
             (r"\textit{foo}_\text{bar}", r"bar"),
             (r"\textit{foo}_\text{foo,bar}", r"foo+1,bar"),
-            (r"\textit{foo}_\text{foo}", r"foo+1"),
             (r"\textit{foo}_\text{foobar,bar_foo,foo}", r"foobar,bar_foo,foo+1"),
             (r"\textit{foo}_\text{foo=bar,foo}", r"foo=bar,foo+1"),
             (r"\textit{foo}_\text{baz,foo,bar,foo}", r"baz,foo+1,bar,foo+1"),
diff --git a/tests/test_backend_latex_backend.py b/tests/test_backend_latex_backend.py
index 365c9e18..56b4fad3 100644
--- a/tests/test_backend_latex_backend.py
+++ b/tests/test_backend_latex_backend.py
@@ -35,7 +35,7 @@ def test_write_before_build(self, no_build, tmpdir_factory):
         )
 
     @pytest.mark.parametrize(
-        ["format", "startswith"],
+        ("format", "startswith"),
         [
             ("tex", "\n\\documentclass{article}"),
             ("rst", "\nObjective"),
@@ -54,13 +54,13 @@ def test_to_file(self, build_all, tmpdir_factory):
         assert Path(filepath).exists()
 
     @pytest.mark.parametrize(
-        ["filepath", "format"],
+        ("filepath", "format"),
         [(None, "foo"), ("myfile.foo", None), ("myfile.tex", "foo")],
     )
     def test_invalid_format(self, build_all, tmpdir_factory, filepath, format):
         if filepath is not None:
             filepath = tmpdir_factory.mktemp("custom_math").join(filepath)
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError) as excinfo:  # noqa: PT011
             build_all.math_documentation.write(filename="foo", format=format)
         assert check_error_or_warning(
             excinfo, "Math documentation format must be one of"
@@ -253,7 +253,7 @@ def test_create_obj_list(self, dummy_latex_backend_model):
         assert dummy_latex_backend_model._create_obj_list("var", "variables") is None
 
     @pytest.mark.parametrize(
-        ["format", "expected"],
+        ("format", "expected"),
         [
             (
                 "tex",
@@ -458,7 +458,7 @@ def test_generate_math_doc_mkdocs_tabbed_not_in_md(self, dummy_model_data):
         )
 
     @pytest.mark.parametrize(
-        ["kwargs", "expected"],
+        ("kwargs", "expected"),
         [
             (
                 {"sets": ["nodes", "techs"]},
@@ -522,7 +522,7 @@ def test_generate_math_string(self, dummy_latex_backend_model, kwargs, expected)
         assert da.math_string == expected
 
     @pytest.mark.parametrize(
-        ["instring", "kwargs", "expected"],
+        ("instring", "kwargs", "expected"),
         [
             ("{{ foo }}", {"foo": 1}, "1"),
             ("{{ foo|removesuffix('s') }}", {"foo": "bars"}, "bar"),
diff --git a/tests/test_backend_parsing.py b/tests/test_backend_parsing.py
index 336615a8..fca9ea41 100644
--- a/tests/test_backend_parsing.py
+++ b/tests/test_backend_parsing.py
@@ -19,7 +19,7 @@ def string_to_dict(yaml_string):
     return yaml_loader.load(StringIO(yaml_string))
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def component_obj():
     setup_string = """
     foreach: [A, A1]
@@ -31,43 +31,43 @@ def component_obj():
     return parsing.ParsedBackendComponent("constraints", "foo", variable_data)
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def exists_array(component_obj, dummy_model_data):
     component_obj.sets = ["nodes", "techs"]
     return component_obj.combine_definition_matrix_and_foreach(dummy_model_data)
 
 
-@pytest.fixture
+@pytest.fixture()
 def valid_component_names(dummy_model_data):
     return ["foo", "bar", "baz", "foobar", *dummy_model_data.data_vars.keys()]
 
 
-@pytest.fixture
+@pytest.fixture()
 def expression_string_parser(valid_component_names):
     return expression_parser.generate_equation_parser(valid_component_names)
 
 
-@pytest.fixture
+@pytest.fixture()
 def arithmetic_string_parser(valid_component_names):
     return expression_parser.generate_arithmetic_parser(valid_component_names)
 
 
-@pytest.fixture
+@pytest.fixture()
 def slice_parser(valid_component_names):
     return expression_parser.generate_slice_parser(valid_component_names)
 
 
-@pytest.fixture
+@pytest.fixture()
 def sub_expression_parser(valid_component_names):
     return expression_parser.generate_sub_expression_parser(valid_component_names)
 
 
-@pytest.fixture
+@pytest.fixture()
 def where_string_parser():
     return where_parser.generate_where_string_parser()
 
 
-@pytest.fixture
+@pytest.fixture()
 def expression_generator():
     def _expression_generator(parse_string, where_string=None):
         expression_dict = {"expression": parse_string}
@@ -78,7 +78,7 @@ def _expression_generator(parse_string, where_string=None):
     return _expression_generator
 
 
-@pytest.fixture
+@pytest.fixture()
 def generate_expression_list(component_obj, expression_string_parser):
     def _generate_expression_list(expression_list, **kwargs):
         return component_obj.generate_expression_list(
@@ -99,7 +99,7 @@ def parse_sub_expressions_and_slices(
     }
 
 
-@pytest.fixture
+@pytest.fixture()
 def parsed_sub_expression_dict(component_obj, sub_expression_parser):
     def _parsed_sub_expression_dict(n_foo, n_bar):
         foos = ", ".join(
@@ -122,7 +122,7 @@ def _parsed_sub_expression_dict(n_foo, n_bar):
     return _parsed_sub_expression_dict
 
 
-@pytest.fixture
+@pytest.fixture()
 def parsed_slice_dict(component_obj, slice_parser):
     def _parsed_slice_dict(n_tech1, n_tech2):
         techs1 = ", ".join(["{where: techs, expression: foo}" for i in range(n_tech1)])
@@ -141,7 +141,7 @@ def _parsed_slice_dict(n_tech1, n_tech2):
     return _parsed_slice_dict
 
 
-@pytest.fixture
+@pytest.fixture()
 def obj_with_sub_expressions_and_slices():
     def _obj_with_sub_expressions_and_slices(equation_string):
         if isinstance(equation_string, str):
@@ -181,7 +181,7 @@ def _obj_with_sub_expressions_and_slices(equation_string):
     return _obj_with_sub_expressions_and_slices
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def equation_obj(expression_string_parser, where_string_parser):
     return parsing.ParsedBackendEquation(
         equation_name="foo",
@@ -191,7 +191,7 @@ def equation_obj(expression_string_parser, where_string_parser):
     )
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def equation_sub_expression_obj(sub_expression_parser, where_string_parser):
     def _equation_sub_expression_obj(name):
         return parsing.ParsedBackendEquation(
@@ -204,7 +204,7 @@ def _equation_sub_expression_obj(name):
     return _equation_sub_expression_obj
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def equation_slice_obj(slice_parser, where_string_parser):
     def _equation_slice_obj(name):
         return parsing.ParsedBackendEquation(
@@ -217,7 +217,7 @@ def _equation_slice_obj(name):
     return _equation_slice_obj
 
 
-@pytest.fixture
+@pytest.fixture()
 def dummy_backend_interface(dummy_model_data):
     # ignore the need to define the abstract methods from backend_model.BackendModel
     with patch.multiple(backend_model.BackendModel, __abstractmethods__=set()):
@@ -239,7 +239,7 @@ def __init__(self):
     return DummyBackendModel()
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def evaluatable_component_obj(valid_component_names):
     def _evaluatable_component_obj(equation_expressions):
         setup_string = f"""
@@ -291,7 +291,7 @@ def evaluate_component_where(
     return component_obj, equation_where_aligned, request.param[1]
 
 
-@pytest.fixture
+@pytest.fixture()
 def evaluate_component_expression(evaluate_component_where, dummy_backend_interface):
     component_obj, equation_where, n_true = evaluate_component_where
 
@@ -325,7 +325,8 @@ def test_parse_string_malformed(
         self, component_obj, expression_string_parser, parse_string
     ):
         parsed_ = component_obj._parse_string(expression_string_parser, parse_string)
-        assert isinstance(parsed_, pp.ParseResults) and len(parsed_) == 0
+        assert isinstance(parsed_, pp.ParseResults)
+        assert len(parsed_) == 0
         assert check_error_or_warning(component_obj._errors, parse_string)
 
     @pytest.mark.parametrize(
@@ -333,7 +334,7 @@ def test_parse_string_malformed(
         ["foo == 1", "$foo + (bar + foobar[A1=a1])**2 >= (dummy_func_1(foo) + 1)"],
     )
     @pytest.mark.parametrize(
-        ["where_string", "expected_where_eval"],
+        ("where_string", "expected_where_eval"),
         [(None, True), ("False", False), ("True or False", True)],
     )
     def test_generate_expression_list(
@@ -467,7 +468,7 @@ def test_extend_equation_list_with_expression_group_missing_sub_expression(
         )
 
     @pytest.mark.parametrize(
-        ["equation_", "expected"],
+        ("equation_", "expected"),
         [("$foo == $bar", False), ("$foo <= $bar", True), ("$foo * 20 >= $bar", True)],
     )
     def test_add_exprs_to_equation_data_multi(
@@ -543,7 +544,7 @@ def test_extend_equation_list_with_expression_group_missing_slices(
         )
 
     @pytest.mark.parametrize(
-        ["eq_string", "expected_n_equations"],
+        ("eq_string", "expected_n_equations"),
         [
             ("1 == bar", 1),
             ([{"expression": "1 == bar"}], 1),
@@ -735,7 +736,7 @@ def test_find_items_in_expression(
         assert found_sub_expressions == {"foo", "bar"}
 
     @pytest.mark.parametrize(
-        ["parse_string", "expected"],
+        ("parse_string", "expected"),
         [
             # sub-expressions in comparisons are always seen
             ("$foo == $bar", ["foo", "bar"]),
@@ -818,7 +819,7 @@ def test_find_single_sub_expression_in_global_expression(
         assert found_sub_expressions == {"foo"}
 
     @pytest.mark.parametrize(
-        ["equation_expr", "sub_expression_exprs"],
+        ("equation_expr", "sub_expression_exprs"),
         [
             ("$foo == 1", {"foo": "foo[techs=$tech1] + bar[techs=$tech2]"}),
             ("$foo == $bar", {"foo": "foo[techs=$tech1]", "bar": "bar[techs=$tech2]"}),
@@ -908,13 +909,12 @@ def test_create_subset_from_where_definitely_empty(
         assert not where.any()
 
     @pytest.mark.parametrize(
-        ["where_string", "expected_where_array"],
+        ("where_string", "expected_where_array"),
         [
             ("with_inf", "with_inf_as_bool"),
             ("only_techs", "only_techs_as_bool"),
             ("with_inf and only_techs", "with_inf_and_only_techs_as_bool"),
             ("with_inf or only_techs", "with_inf_or_only_techs_as_bool"),
-            ("with_inf or only_techs", "with_inf_or_only_techs_as_bool"),
             (
                 "with_inf and [bar] in nodes",
                 "with_inf_as_bool_and_subset_on_bar_in_nodes",
@@ -992,7 +992,7 @@ def test_evaluate_expression(self, evaluate_component_expression):
 
 
 class TestParsedConstraint:
-    @pytest.fixture
+    @pytest.fixture()
     def constraint_obj(self):
         dict_ = {
             "foreach": ["techs"],
@@ -1044,7 +1044,7 @@ def test_parse_constraint_dict_evaluate_eq2(
 
 
 class TestParsedVariable:
-    @pytest.fixture
+    @pytest.fixture()
     def variable_obj(self):
         dict_ = {"foreach": ["techs"], "where": "False"}
 
@@ -1066,7 +1066,7 @@ def test_parse_variable_dict_empty_eq1(
 
 
 class TestParsedObjective:
-    @pytest.fixture
+    @pytest.fixture()
     def objective_obj(self):
         dict_ = {
             "equations": [
diff --git a/tests/test_backend_pyomo.py b/tests/test_backend_pyomo.py
index 64683cd8..50078a55 100755
--- a/tests/test_backend_pyomo.py
+++ b/tests/test_backend_pyomo.py
@@ -18,7 +18,7 @@
 
 @pytest.mark.xfail(reason="Not expecting operate mode to work at the moment")
 class TestChecks:
-    @pytest.mark.parametrize("on", (True, False))
+    @pytest.mark.parametrize("on", [True, False])
     def test_operate_cyclic_storage(self, on):
         """Cannot have cyclic storage in operate mode"""
         if on is True:
@@ -60,7 +60,7 @@ def test_loading_timeseries_operate_efficiencies(self, param):
         with pytest.warns(exceptions.ModelWarning):
             m.build()  # will fail to complete run if there's a problem
 
-    @pytest.mark.parametrize("constr", ("max", "equals"))
+    @pytest.mark.parametrize("constr", ["max", "equals"])
     def test_operate_flow_out_min_relative(self, constr):
         """If we depend on a finite flow_cap, we have to error on a user failing to define it"""
         m = build_model(
@@ -84,7 +84,7 @@ def test_operate_flow_out_min_relative(self, constr):
             error, ["Operate mode: User must define a finite flow_cap"]
         )
 
-    @pytest.mark.parametrize("constr", ("max", "equals"))
+    @pytest.mark.parametrize("constr", ["max", "equals"])
     def test_operate_flow_cap_source_unit(self, constr):
         """If we depend on a finite flow_cap, we have to error on a user failing to define it"""
         m = build_model(
@@ -112,7 +112,7 @@ def test_operate_flow_cap_source_unit(self, constr):
                 m.build()
 
     @pytest.mark.parametrize(
-        ["source_unit", "constr"],
+        ("source_unit", "constr"),
         list(product(("absolute", "per_cap", "per_area"), ("max", "equals"))),
     )
     def test_operate_source_unit_with_area_use(self, source_unit, constr):
@@ -237,10 +237,9 @@ def test_operate_storage(self, param):
             ],
         )
 
-    @pytest.mark.parametrize("on", (True, False))
+    @pytest.mark.parametrize("on", [True, False])
     def test_operate_source_cap_max(self, on):
         """Some constraints, if not defined, will throw a warning and possibly change values in model_data"""
-
         if on is False:
             override = {}
         else:
@@ -264,10 +263,9 @@ def test_operate_source_cap_max(self, on):
             )
             assert m._model_data.source_cap.loc["a", "test_supply_plus"].item() == 1e6
 
-    @pytest.mark.parametrize("on", (True, False))
+    @pytest.mark.parametrize("on", [True, False])
     def test_operate_storage_initial(self, on):
         """Some constraints, if not defined, will throw a warning and possibly change values in model_data"""
-
         if on is False:
             override = {}
         else:
@@ -295,16 +293,11 @@ def test_operate_storage_initial(self, on):
 @pytest.mark.skip(reason="to be reimplemented by comparison to LP files")
 class TestBalanceConstraints:
     def test_loc_carriers_system_balance_constraint(self, simple_supply):
-        """
-        sets.loc_carriers
-        """
-
+        """sets.loc_carriers"""
         assert "system_balance" in simple_supply.backend.constraints
 
     def test_loc_techs_balance_supply_constraint(self):
-        """
-        sets.loc_techs_finite_resource_supply,
-        """
+        """sets.loc_techs_finite_resource_supply,"""
         m = build_model(
             {"techs.test_supply_elec.constraints.resource": 20},
             "simple_supply,two_hours,investment_costs",
@@ -339,9 +332,7 @@ def test_loc_techs_balance_supply_constraint(self):
         )
 
     def test_loc_techs_balance_demand_constraint(self, simple_supply):
-        """
-        sets.loc_techs_finite_resource_demand,
-        """
+        """sets.loc_techs_finite_resource_demand,"""
         assert "balance_demand" in simple_supply.backend.constraints
 
         m = build_model(
@@ -367,9 +358,7 @@ def test_loc_techs_balance_demand_constraint(self, simple_supply):
     def test_loc_techs_resource_availability_supply_plus_constraint(
         self, simple_supply_and_supply_plus
     ):
-        """
-        sets.loc_techs_finite_resource_supply_plus,
-        """
+        """sets.loc_techs_finite_resource_supply_plus,"""
         assert (
             "resource_availability_supply_plus"
             in simple_supply_and_supply_plus.backend.constraints
@@ -400,18 +389,13 @@ def test_loc_techs_resource_availability_supply_plus_constraint(
         )
 
     def test_loc_techs_balance_transmission_constraint(self, simple_supply):
-        """
-        sets.loc_techs_transmission,
-        """
+        """sets.loc_techs_transmission,"""
         assert "balance_transmission" in simple_supply.backend.constraints
 
     def test_loc_techs_balance_supply_plus_constraint(
         self, simple_supply_and_supply_plus
     ):
-        """
-        sets.loc_techs_supply_plus,
-        """
-
+        """sets.loc_techs_supply_plus,"""
         assert (
             "balance_supply_plus_with_storage"
             in simple_supply_and_supply_plus.backend.constraints
@@ -422,16 +406,12 @@ def test_loc_techs_balance_supply_plus_constraint(
         )
 
     def test_loc_techs_balance_storage_constraint(self, simple_storage):
-        """
-        sets.loc_techs_storage,
-        """
+        """sets.loc_techs_storage,"""
         assert "balance_storage" in simple_storage.backend.constraints
         assert "set_storage_initial" not in simple_storage.backend.constraints
 
     def test_loc_techs_balance_storage_discharge_depth_constraint(self):
-        """
-        sets.loc_techs_storage,
-        """
+        """sets.loc_techs_storage,"""
         m = build_model(
             {}, "simple_storage,two_hours,investment_costs,storage_discharge_depth"
         )
@@ -450,9 +430,7 @@ def test_loc_techs_balance_storage_discharge_depth_constraint(self):
         ).all()
 
     def test_storage_initial_constraint(self, simple_storage):
-        """
-        sets.loc_techs_store,
-        """
+        """sets.loc_techs_store,"""
         assert "balance_storage" in simple_storage.backend.constraints
         assert "set_storage_initial" not in simple_storage.backend.constraints
 
@@ -466,9 +444,7 @@ def test_storage_initial_constraint(self, simple_storage):
 
     @pytest.mark.xfail(reason="no longer a constraint we're creating")
     def test_carriers_reserve_margin_constraint(self):
-        """
-        i for i in sets.carriers if i in model_run.model.get_key('reserve_margin', {}).keys()
-        """
+        """I for i in sets.carriers if i in model_run.model.get_key('reserve_margin', {}).keys()"""
         m = build_model(
             {"model.reserve_margin.electricity": 0.01},
             "simple_supply,two_hours,investment_costs",
@@ -481,15 +457,11 @@ def test_carriers_reserve_margin_constraint(self):
 class TestCostConstraints:
     # costs.py
     def test_loc_techs_cost_constraint(self, simple_supply):
-        """
-        sets.loc_techs_cost,
-        """
+        """sets.loc_techs_cost,"""
         assert "cost" in simple_supply.backend.expressions
 
     def test_loc_techs_cost_investment_constraint(self, simple_conversion):
-        """
-        sets.loc_techs_investment_cost,
-        """
+        """sets.loc_techs_investment_cost,"""
         assert "cost_investment" in simple_conversion.backend.expressions
 
     def test_loc_techs_cost_investment_milp_constraint(self):
@@ -505,15 +477,12 @@ def test_loc_techs_cost_investment_milp_constraint(self):
         assert "cost_investment" in m.backend.expressions
 
     def test_loc_techs_not_cost_var_constraint(self, simple_conversion):
-        """
-        i for i in sets.loc_techs_om_cost if i not in sets.loc_techs_conversion_plus + sets.loc_techs_conversion
-
-        """
+        """I for i in sets.loc_techs_om_cost if i not in sets.loc_techs_conversion_plus + sets.loc_techs_conversion"""
         assert "cost_var" not in simple_conversion.backend.expressions
 
     @pytest.mark.parametrize(
-        "tech,scenario,cost",
-        (
+        ("tech", "scenario", "cost"),
+        [
             ("test_supply_elec", "simple_supply", "flow_out"),
             ("test_supply_elec", "simple_supply", "flow_in"),
             ("test_supply_plus", "simple_supply_and_supply_plus", "flow_in"),
@@ -521,13 +490,10 @@ def test_loc_techs_not_cost_var_constraint(self, simple_conversion):
             ("test_transmission_elec", "simple_supply", "flow_out"),
             ("test_conversion", "simple_conversion", "flow_in"),
             ("test_conversion_plus", "simple_conversion_plus", "flow_out"),
-        ),
+        ],
     )
     def test_loc_techs_cost_var_constraint(self, tech, scenario, cost):
-        """
-        i for i in sets.loc_techs_om_cost if i not in sets.loc_techs_conversion_plus + sets.loc_techs_conversion
-
-        """
+        """I for i in sets.loc_techs_om_cost if i not in sets.loc_techs_conversion_plus + sets.loc_techs_conversion"""
         m = build_model(
             {"techs.{}.costs.monetary.{}".format(tech, cost): 1},
             "{},two_hours".format(scenario),
@@ -536,9 +502,7 @@ def test_loc_techs_cost_var_constraint(self, tech, scenario, cost):
         assert "cost_var" in m.backend.expressions
 
     def test_one_way_om_cost(self):
-        """
-        With one_way transmission, it should still be possible to set an flow_out cost.
-        """
+        """With one_way transmission, it should still be possible to set an flow_out cost."""
         m = build_model(
             {
                 "techs.test_transmission_elec.costs.monetary.flow_out": 1,
@@ -568,12 +532,10 @@ def test_one_way_om_cost(self):
 class TestExportConstraints:
     # export.py
     def test_loc_carriers_system_balance_no_export(self, simple_supply):
-        """
-        i for i in sets.loc_carriers if sets.loc_techs_export
+        """I for i in sets.loc_carriers if sets.loc_techs_export
         and any(['{0}::{2}'.format(*j.split('::')) == i
         for j in sets.loc_tech_carriers_export])
         """
-
         assert not check_variable_exists(
             simple_supply.backend.get_constraint(
                 "system_balance", as_backend_objs=False
@@ -590,15 +552,11 @@ def test_loc_carriers_system_balance_export(self, supply_export):
         )
 
     def test_loc_tech_carriers_export_balance_constraint(self, supply_export):
-        """
-        sets.loc_tech_carriers_export,
-        """
+        """sets.loc_tech_carriers_export,"""
         assert "export_balance" in supply_export.backend.constraints
 
     def test_loc_techs_update_costs_var_constraint(self, supply_export):
-        """
-        i for i in sets.loc_techs_om_cost if i in sets.loc_techs_export
-        """
+        """I for i in sets.loc_techs_om_cost if i in sets.loc_techs_export"""
         assert "cost_var" in supply_export.backend.expressions
 
         m = build_model(
@@ -613,11 +571,9 @@ def test_loc_techs_update_costs_var_constraint(self, supply_export):
         )
 
     def test_loc_tech_carriers_export_max_constraint(self):
-        """
-        i for i in sets.loc_tech_carriers_export
+        """I for i in sets.loc_tech_carriers_export
         if constraint_exists(model_run, i.rsplit('::', 1)[0], 'constraints.export_max')
         """
-
         m = build_model(
             {"techs.test_supply_elec.constraints.export_max": 5},
             "supply_export,two_hours,investment_costs",
@@ -633,9 +589,7 @@ class TestCapacityConstraints:
     def test_loc_techs_storage_capacity_constraint(
         self, simple_storage, simple_supply_and_supply_plus
     ):
-        """
-        i for i in sets.loc_techs_store if i not in sets.loc_techs_milp
-        """
+        """I for i in sets.loc_techs_store if i not in sets.loc_techs_milp"""
         assert "storage_max" in simple_storage.backend.constraints
         assert "storage_max" in simple_supply_and_supply_plus.backend.constraints
 
@@ -672,7 +626,7 @@ def test_loc_techs_storage_capacity_milp_constraint(self):
         assert "storage_capacity" not in m.backend.constraints
 
     @pytest.mark.parametrize(
-        "scenario,tech,override",
+        ("scenario", "tech", "override"),
         [
             i + (j,)
             for i in [
@@ -683,9 +637,7 @@ def test_loc_techs_storage_capacity_milp_constraint(self):
         ],
     )
     def test_loc_techs_flow_capacity_storage_constraint(self, scenario, tech, override):
-        """
-        i for i in sets.loc_techs_store if constraint_exists(model_run, i, 'constraints.flow_cap_per_storage_cap_max')
-        """
+        """I for i in sets.loc_techs_store if constraint_exists(model_run, i, 'constraints.flow_cap_per_storage_cap_max')"""
         m = build_model(
             {f"techs.{tech}.constraints.flow_cap_per_storage_cap_{override}": 0.5},
             f"{scenario},two_hours,investment_costs",
@@ -697,12 +649,9 @@ def test_loc_techs_flow_capacity_storage_constraint(self, scenario, tech, overri
         )
 
     @pytest.mark.filterwarnings("ignore:(?s).*Integer:calliope.exceptions.ModelWarning")
-    @pytest.mark.parametrize("override", (("max", "min")))
+    @pytest.mark.parametrize("override", (["max", "min"]))
     def test_loc_techs_flow_capacity_milp_storage_constraint(self, override):
-        """
-        i for i in sets.loc_techs_store if constraint_exists(model_run, i, 'constraints.flow_cap_per_storage_cap_max')
-        """
-
+        """I for i in sets.loc_techs_store if constraint_exists(model_run, i, 'constraints.flow_cap_per_storage_cap_max')"""
         m = build_model(
             {
                 f"techs.test_supply_plus.constraints.flow_cap_per_storage_cap_{override}": 0.5
@@ -716,9 +665,7 @@ def test_loc_techs_flow_capacity_milp_storage_constraint(self, override):
         )
 
     def test_no_loc_techs_flow_capacity_storage_constraint(self, caplog):
-        """
-        i for i in sets.loc_techs_store if constraint_exists(model_run, i, 'constraints.flow_cap_per_storage_cap_max')
-        """
+        """I for i in sets.loc_techs_store if constraint_exists(model_run, i, 'constraints.flow_cap_per_storage_cap_max')"""
         with caplog.at_level(logging.INFO):
             m = build_model(model_file="flow_cap_per_storage_cap.yaml")
 
@@ -732,15 +679,13 @@ def test_no_loc_techs_flow_capacity_storage_constraint(self, caplog):
             ]
         )
 
-    @pytest.mark.parametrize("override", ((None, "max", "min")))
+    @pytest.mark.parametrize("override", ([None, "max", "min"]))
     def test_loc_techs_resource_capacity_constraint(self, override):
-        """
-        i for i in sets.loc_techs_finite_resource_supply_plus
+        """I for i in sets.loc_techs_finite_resource_supply_plus
         if any([constraint_exists(model_run, i, 'constraints.resource_cap_equals'),
                 constraint_exists(model_run, i, 'constraints.resource_cap_max'),
                 constraint_exists(model_run, i, 'constraints.resource_cap_min')])
         """
-
         if override is None:
             m = build_model(
                 {}, "simple_supply_and_supply_plus,two_hours,investment_costs"
@@ -775,8 +720,7 @@ def test_loc_techs_resource_capacity_constraint(self, override):
     def test_loc_techs_resource_capacity_equals_flow_capacity_constraint(
         self, simple_supply_and_supply_plus
     ):
-        """
-        i for i in sets.loc_techs_finite_resource_supply_plus
+        """I for i in sets.loc_techs_finite_resource_supply_plus
         if constraint_exists(model_run, i, 'constraints.resource_cap_equals_flow_cap')
         """
         assert (
@@ -792,9 +736,7 @@ def test_loc_techs_resource_capacity_equals_flow_capacity_constraint(
         assert "resource_capacity_equals_flow_capacity" in m.backend.constraints
 
     def test_loc_techs_area_use_constraint(self, simple_supply_and_supply_plus):
-        """
-        i for i in sets.loc_techs_area if i in sets.loc_techs_supply_plus
-        """
+        """I for i in sets.loc_techs_area if i in sets.loc_techs_supply_plus"""
         assert "area_use" not in simple_supply_and_supply_plus.backend.variables
 
         m = build_model(
@@ -829,8 +771,7 @@ def test_loc_techs_area_use_constraint(self, simple_supply_and_supply_plus):
     def test_loc_techs_area_use_per_flow_capacity_constraint(
         self, simple_supply_and_supply_plus
     ):
-        """
-        i for i in sets.loc_techs_area if i in sets.loc_techs_supply_plus
+        """I for i in sets.loc_techs_area if i in sets.loc_techs_supply_plus
         and constraint_exists(model_run, i, 'constraints.area_use_per_flow_cap')
         """
         assert (
@@ -867,8 +808,7 @@ def test_loc_techs_area_use_per_flow_capacity_constraint(
     def test_locs_area_use_capacity_per_loc_constraint(
         self, simple_supply_and_supply_plus
     ):
-        """
-        i for i in sets.locs
+        """I for i in sets.locs
         if model_run.nodes[i].get_key('available_area', None) is not None
         """
         assert (
@@ -895,8 +835,7 @@ def test_locs_area_use_capacity_per_loc_constraint(
 
     @pytest.mark.xfail(reason="flow_cap_scale is no more")
     def test_loc_techs_flow_capacity_constraint(self, simple_supply_and_supply_plus):
-        """
-        i for i in sets.loc_techs
+        """I for i in sets.loc_techs
         if i not in sets.loc_techs_milp + sets.loc_techs_purchase
         """
         m2 = build_model(
@@ -939,8 +878,8 @@ def test_loc_techs_flow_capacity_constraint_warning_on_infinite_equals(self):
         override = {
             "nodes.a.techs.test_supply_elec.constraints.flow_cap_equals": np.inf
         }
+        m = build_model(override, "simple_supply,two_hours,investment_costs")
         with pytest.raises(exceptions.ModelError) as error:
-            m = build_model(override, "simple_supply,two_hours,investment_costs")
             m.build()
 
         assert check_error_or_warning(
@@ -948,10 +887,9 @@ def test_loc_techs_flow_capacity_constraint_warning_on_infinite_equals(self):
             "Cannot use inf for flow_cap_equals for node, tech `('a', 'test_supply_elec')`",
         )
 
-    @pytest.mark.parametrize("bound", (("max")))
+    @pytest.mark.parametrize("bound", ("max"))
     def test_techs_flow_capacity_systemwide_constraint(self, bound):
-        """
-        i for i in sets.techs
+        """I for i in sets.techs
         if model_run.get_key('techs.{}.constraints.flow_cap_max_systemwide'.format(i), None)
         """
 
@@ -995,7 +933,7 @@ def check_bounds(constraint):
             ).sel(techs="test_supply_elec")
         )
 
-    @pytest.mark.parametrize("bound", (("equals", "max")))
+    @pytest.mark.parametrize("bound", (["equals", "max"]))
     def test_techs_flow_capacity_systemwide_no_constraint(self, simple_supply, bound):
         assert "flow_capacity_systemwide" not in simple_supply.backend.constraints
 
@@ -1012,8 +950,7 @@ def test_techs_flow_capacity_systemwide_no_constraint(self, simple_supply, bound
 class TestDispatchConstraints:
     # dispatch.py
     def test_loc_tech_carriers_flow_out_max_constraint(self, simple_supply):
-        """
-        i for i in sets.loc_tech_carriers_prod
+        """I for i in sets.loc_tech_carriers_prod
         if i not in sets.loc_tech_carriers_conversion_plus
         and i.rsplit('::', 1)[0] not in sets.loc_techs_milp
         """
@@ -1023,8 +960,7 @@ def test_loc_tech_carriers_flow_out_max_milp_constraint(self, supply_milp):
         assert "flow_out_max" not in supply_milp.backend.constraints
 
     def test_loc_tech_carriers_flow_out_min_constraint(self, simple_supply):
-        """
-        i for i in sets.loc_tech_carriers_prod
+        """I for i in sets.loc_tech_carriers_prod
         if i not in sets.loc_tech_carriers_conversion_plus
         and constraint_exists(model_run, i, 'constraints.flow_out_min_relative')
         and i.rsplit('::', 1)[0] not in sets.loc_techs_milp
@@ -1049,8 +985,7 @@ def test_loc_tech_carriers_flow_out_min_milp_constraint(self, supply_milp):
         assert "flow_out_min" not in m.backend.constraints
 
     def test_loc_tech_carriers_flow_in_max_constraint(self, simple_supply):
-        """
-        i for i in sets.loc_tech_carriers_con
+        """I for i in sets.loc_tech_carriers_con
         if i.rsplit('::', 1)[0] in sets.loc_techs_demand +
             sets.loc_techs_storage + sets.loc_techs_transmission
         and i.rsplit('::', 1)[0] not in sets.loc_techs_milp
@@ -1063,9 +998,7 @@ def test_loc_tech_carriers_flow_in_max_milp_constraint(self, supply_milp):
     def test_loc_techs_resource_max_constraint(
         self, simple_supply, simple_supply_and_supply_plus
     ):
-        """
-        sets.loc_techs_finite_resource_supply_plus,
-        """
+        """sets.loc_techs_finite_resource_supply_plus,"""
         assert "resource_max" not in simple_supply.backend.constraints
         assert "resource_max" in simple_supply_and_supply_plus.backend.constraints
 
@@ -1079,16 +1012,13 @@ def test_loc_techs_resource_max_constraint(
     def test_loc_techs_storage_max_constraint(
         self, simple_supply, simple_supply_and_supply_plus, simple_storage
     ):
-        """
-        sets.loc_techs_store
-        """
+        """sets.loc_techs_store"""
         assert "storage_max" not in simple_supply.backend.constraints
         assert "storage_max" in simple_supply_and_supply_plus.backend.constraints
         assert "storage_max" in simple_storage.backend.constraints
 
     def test_loc_tech_carriers_ramping_constraint(self, simple_supply):
-        """
-        i for i in sets.loc_tech_carriers_prod
+        """I for i in sets.loc_tech_carriers_prod
         if i.rsplit('::', 1)[0] in sets.loc_techs_ramping
         """
         assert "ramping_up" not in simple_supply.backend.constraints
@@ -1117,9 +1047,7 @@ class TestMILPConstraints:
     def test_loc_techs_unit_commitment_milp_constraint(
         self, simple_supply, supply_milp, supply_purchase
     ):
-        """
-        sets.loc_techs_milp,
-        """
+        """sets.loc_techs_milp,"""
         assert "unit_commitment_milp" not in simple_supply.backend.constraints
         assert "unit_commitment_milp" in supply_milp.backend.constraints
         assert "unit_commitment_milp" not in supply_purchase.backend.constraints
@@ -1127,9 +1055,7 @@ def test_loc_techs_unit_commitment_milp_constraint(
     def test_loc_techs_unit_capacity_milp_constraint(
         self, simple_supply, supply_milp, supply_purchase
     ):
-        """
-        sets.loc_techs_milp,
-        """
+        """sets.loc_techs_milp,"""
         assert "units" not in simple_supply.backend.variables
         assert "units" in supply_milp.backend.variables
         assert "units" not in supply_purchase.backend.variables
@@ -1137,8 +1063,7 @@ def test_loc_techs_unit_capacity_milp_constraint(
     def test_loc_tech_carriers_flow_out_max_milp_constraint(
         self, simple_supply, supply_milp, supply_purchase, conversion_plus_milp
     ):
-        """
-        i for i in sets.loc_tech_carriers_prod
+        """I for i in sets.loc_tech_carriers_prod
         if i not in sets.loc_tech_carriers_conversion_plus
         and i.rsplit('::', 1)[0] in sets.loc_techs_milp
         """
@@ -1156,11 +1081,9 @@ def test_loc_techs_flow_out_max_conversion_plus_milp_constraint(
         conversion_plus_milp,
         conversion_plus_purchase,
     ):
-        """
-        i for i in sets.loc_techs_conversion_plus
+        """I for i in sets.loc_techs_conversion_plus
         if i in sets.loc_techs_milp
         """
-
         assert (
             "flow_out_max_conversion_plus_milp" not in simple_supply.backend.constraints
         )
@@ -1181,8 +1104,7 @@ def test_loc_techs_flow_out_max_conversion_plus_milp_constraint(
         )
 
     def test_loc_tech_carriers_flow_out_min_milp_constraint(self):
-        """
-        i for i in sets.loc_tech_carriers_prod
+        """I for i in sets.loc_tech_carriers_prod
         if i not in sets.loc_tech_carriers_conversion_plus
         and constraint_exists(model_run, i.rsplit('::', 1)[0], 'constraints.flow_out_min_relative')
         and i.rsplit('::', 1)[0] in sets.loc_techs_milp
@@ -1223,8 +1145,7 @@ def test_loc_tech_carriers_flow_out_min_milp_constraint(self):
         assert "flow_out_min_milp" not in m.backend.constraints
 
     def test_loc_techs_flow_out_min_conversion_plus_milp_constraint(self):
-        """
-        i for i in sets.loc_techs_conversion_plus
+        """I for i in sets.loc_techs_conversion_plus
         if constraint_exists(model_run, i, 'constraints.flow_out_min_relative')
         and i in sets.loc_techs_milp
         """
@@ -1266,8 +1187,7 @@ def test_loc_techs_flow_out_min_conversion_plus_milp_constraint(self):
     def test_loc_tech_carriers_flow_in_max_milp_constraint(
         self, simple_supply, supply_milp, storage_milp, conversion_plus_milp
     ):
-        """
-        i for i in sets.loc_tech_carriers_con
+        """I for i in sets.loc_tech_carriers_con
         if i.rsplit('::', 1)[0] in sets.loc_techs_demand +
             sets.loc_techs_storage + sets.loc_techs_transmission
         and i.rsplit('::', 1)[0] in sets.loc_techs_milp
@@ -1280,8 +1200,7 @@ def test_loc_tech_carriers_flow_in_max_milp_constraint(
     def test_loc_techs_flow_capacity_units_milp_constraint(
         self, simple_supply, supply_milp, storage_milp, conversion_plus_milp
     ):
-        """
-        i for i in sets.loc_techs_milp
+        """I for i in sets.loc_techs_milp
         if constraint_exists(model_run, i, 'constraints.flow_cap_per_unit')
         is not None
         """
@@ -1298,9 +1217,7 @@ def test_loc_techs_storage_capacity_units_milp_constraint(
         conversion_plus_milp,
         supply_and_supply_plus_milp,
     ):
-        """
-        i for i in sets.loc_techs_milp if i in sets.loc_techs_store
-        """
+        """I for i in sets.loc_techs_milp if i in sets.loc_techs_store"""
         assert "storage_capacity_units_milp" not in simple_supply.backend.constraints
         assert "storage_capacity_units_milp" not in supply_milp.backend.constraints
         assert "storage_capacity_units_milp" in storage_milp.backend.constraints
@@ -1317,8 +1234,7 @@ def test_loc_techs_storage_capacity_units_milp_constraint(
     def test_loc_techs_flow_capacity_max_purchase_milp_constraint(
         self, simple_supply, supply_milp, supply_purchase
     ):
-        """
-        i for i in sets.loc_techs_purchase
+        """I for i in sets.loc_techs_purchase
         if (constraint_exists(model_run, i, 'constraints.flow_cap_equals') is not None
             or constraint_exists(model_run, i, 'constraints.flow_cap_max') is not None)
         """
@@ -1343,8 +1259,7 @@ def test_loc_techs_flow_capacity_max_purchase_milp_constraint(
     def test_loc_techs_flow_capacity_min_purchase_milp_constraint(
         self, simple_supply, supply_milp, supply_purchase
     ):
-        """
-        i for i in sets.loc_techs_purchase
+        """I for i in sets.loc_techs_purchase
         if (not constraint_exists(model_run, i, 'constraints.flow_cap_equals')
             and constraint_exists(model_run, i, 'constraints.flow_cap_min'))
         """
@@ -1378,9 +1293,7 @@ def test_loc_techs_flow_capacity_min_purchase_milp_constraint(
     def test_loc_techs_storage_capacity_max_purchase_milp_constraint(
         self, simple_storage, storage_milp, storage_purchase, supply_purchase
     ):
-        """
-        i for i in set(sets.loc_techs_purchase).intersection(sets.loc_techs_store)
-        """
+        """I for i in set(sets.loc_techs_purchase).intersection(sets.loc_techs_store)"""
         assert (
             "storage_capacity_max_purchase_milp"
             not in simple_storage.backend.constraints
@@ -1399,8 +1312,7 @@ def test_loc_techs_storage_capacity_max_purchase_milp_constraint(
     def test_loc_techs_storage_capacity_min_purchase_milp_constraint(
         self, storage_purchase
     ):
-        """
-        i for i in set(sets.loc_techs_purchase).intersection(sets.loc_techs_store)
+        """I for i in set(sets.loc_techs_purchase).intersection(sets.loc_techs_store)
         if (not constraint_exists(model_run, i, 'constraints.storage_cap_equals')
             and (constraint_exists(model_run, i, 'constraints.storage_cap_min')
                 or constraint_exists(model_run, i, 'constraints.flow_cap_min')))
@@ -1433,7 +1345,7 @@ def test_loc_techs_storage_capacity_min_purchase_milp_constraint(
 
     @pytest.mark.parametrize(
         ("scenario", "exists", "override_dict"),
-        (
+        [
             ("simple_supply", ("not", "not"), {}),
             ("supply_milp", ("not", "not"), {}),
             (
@@ -1442,18 +1354,16 @@ def test_loc_techs_storage_capacity_min_purchase_milp_constraint(
                 {"techs.test_supply_elec.costs.monetary.purchase": 1},
             ),
             ("supply_purchase", ("is", "not"), {}),
-        ),
+        ],
     )
     def test_loc_techs_update_costs_investment_units_milp_constraint(
         self, scenario, exists, override_dict
     ):
-        """
-        i for i in sets.loc_techs_milp
+        """I for i in sets.loc_techs_milp
         if i in sets.loc_techs_investment_cost and
         any(constraint_exists(model_run, i, 'costs.{}.purchase'.format(j))
                for j in model_run.sets.costs)
         """
-
         m = build_model(override_dict, f"{scenario},two_hours,investment_costs")
         m.build()
         if exists[0] == "not":
@@ -1478,10 +1388,7 @@ def test_loc_techs_update_costs_investment_units_milp_constraint(
             )
 
     def test_techs_unit_capacity_max_systemwide_milp_constraint(self):
-        """
-        sets.techs if unit_cap_max_systemwide or unit_cap_equals_systemwide
-        """
-
+        """sets.techs if unit_cap_max_systemwide or unit_cap_equals_systemwide"""
         override_max = {
             "links.a,b.exists": True,
             "techs.test_conversion_plus.constraints.units_max_systemwide": 2,
@@ -1507,9 +1414,7 @@ def test_techs_unit_capacity_max_systemwide_milp_constraint(self):
         reason="systemwide constraints now don't work with transmission techs, since transmission tech names are now never independent of a node"
     )
     def test_techs_unit_capacity_max_systemwide_transmission_milp_constraint(self):
-        """
-        sets.techs if unit_cap_max_systemwide or unit_cap_equals_systemwide
-        """
+        """sets.techs if unit_cap_max_systemwide or unit_cap_equals_systemwide"""
         override_transmission = {
             "links.a,b.exists": True,
             "techs.test_transmission_elec.constraints": {
@@ -1549,10 +1454,9 @@ def test_techs_unit_capacity_max_systemwide_no_transmission_milp_constraint(self
         m.build()
         assert "unit_capacity_max_systemwide_milp" in m.backend.constraints
 
-    @pytest.mark.parametrize("tech", (("test_storage"), ("test_transmission_elec")))
+    @pytest.mark.parametrize("tech", [("test_storage"), ("test_transmission_elec")])
     def test_asynchronous_flow_constraint(self, tech):
-        """
-        Binary switch for flow in/out can be activated using the option
+        """Binary switch for flow in/out can be activated using the option
         'asynchronous_flow'
         """
         m = build_model(
@@ -1571,9 +1475,7 @@ class TestConversionConstraints:
     def test_loc_techs_balance_conversion_constraint(
         self, simple_supply, simple_conversion, simple_conversion_plus
     ):
-        """
-        sets.loc_techs_conversion,
-        """
+        """sets.loc_techs_conversion,"""
         assert "balance_conversion" not in simple_supply.backend.constraints
         assert "balance_conversion" in simple_conversion.backend.constraints
         assert "balance_conversion" not in simple_conversion_plus.backend.constraints
@@ -1585,9 +1487,7 @@ class TestNetworkConstraints:
     def test_loc_techs_symmetric_transmission_constraint(
         self, simple_supply, simple_conversion_plus
     ):
-        """
-        sets.loc_techs_transmission,
-        """
+        """sets.loc_techs_transmission,"""
         assert "symmetric_transmission" in simple_supply.backend.constraints
         assert (
             "symmetric_transmission" not in simple_conversion_plus.backend.constraints
@@ -1700,9 +1600,7 @@ def test_source_equals_cannot_be_inf(self):
         assert check_error_or_warning(excinfo, "Cannot include infinite values")
 
     def test_storage_initial_fractional_value(self):
-        """
-        Check that the storage_initial value is a fraction
-        """
+        """Check that the storage_initial value is a fraction"""
         m = build_model(
             {"techs.test_storage.storage_initial": 5},
             "simple_storage,two_hours,investment_costs",
@@ -1723,7 +1621,7 @@ def simple_supply_longnames(self):
         m.backend.verbose_strings()
         return m
 
-    @pytest.fixture
+    @pytest.fixture()
     def temp_path(self, tmpdir_factory):
         return tmpdir_factory.mktemp("custom_math")
 
@@ -2009,8 +1907,7 @@ def test_raise_error_on_preexistence_diff_type(self, simple_supply):
         )
 
     def test_raise_error_on_constraint_with_nan(self, simple_supply):
-        """
-        A very simple constraint: For each tech, let the annual and regional sum of `flow_out` be larger than 100.
+        """A very simple constraint: For each tech, let the annual and regional sum of `flow_out` be larger than 100.
         However, not every tech has the variable `flow_out`.
         How to solve it? Let the constraint be active only where flow_out exists by setting 'where' accordingly.
         """
@@ -2051,8 +1948,7 @@ def test_raise_error_on_constraint_with_nan(self, simple_supply):
         )
 
     def test_add_global_expression(self, simple_supply):
-        """
-        A very simple expression: The annual and regional sum of `flow_out` for each tech.
+        """A very simple expression: The annual and regional sum of `flow_out` for each tech.
         However, not every tech has the variable `flow_out`.
         How to solve it? Let the constraint be active only where flow_out exists by setting 'where' accordingly.
         """
@@ -2073,8 +1969,7 @@ def test_add_global_expression(self, simple_supply):
         )
 
     def test_raise_error_on_excess_dimensions(self, simple_supply):
-        """
-        A very simple constraint: For each tech, let the `flow_cap` be larger than 100.
+        """A very simple constraint: For each tech, let the `flow_cap` be larger than 100.
         However, we forgot to include `nodes` in `foreach`.
         With `nodes` included, this constraint should build.
         """
@@ -2131,7 +2026,7 @@ def test_delete_inexistent_pyomo_list(self, simple_supply, component):
         assert "bar" not in getattr(backend_instance, component).keys()
 
     @pytest.mark.parametrize(
-        ["component", "eq"],
+        ("component", "eq"),
         [("global_expressions", "flow_cap + 1"), ("constraints", "flow_cap >= 1")],
     )
     def test_add_allnull_expr_or_constr(self, simple_supply, component, eq):
@@ -2212,7 +2107,7 @@ def test_object_string_representation(self, simple_supply):
         assert not simple_supply.backend.variables.flow_out.coords_in_name
 
     @pytest.mark.parametrize(
-        ["objname", "dims", "objtype"],
+        ("objname", "dims", "objtype"),
         [
             (
                 "flow_out",
@@ -2318,9 +2213,7 @@ def test_update_parameter_replace_defaults(self, simple_supply):
         assert expected.equals(updated_param)
 
     def test_update_parameter_add_dim(self, caplog, simple_supply):
-        """
-        flow_out_eff doesn't have the time dimension in the simple model, we add it here.
-        """
+        """flow_out_eff doesn't have the time dimension in the simple model, we add it here."""
         updated_param = simple_supply.inputs.flow_out_eff.where(
             simple_supply.inputs.timesteps.notnull()
         )
@@ -2485,31 +2378,31 @@ def test_has_integer_or_binary_variables_milp(self, supply_milp):
 
 
 class TestShadowPrices:
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def simple_supply(self):
         m = build_model({}, "simple_supply,two_hours,investment_costs")
         m.build()
         return m
 
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def supply_milp(self):
         m = build_model({}, "supply_milp,two_hours,investment_costs")
         m.build()
         return m
 
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def simple_supply_with_yaml_shadow_prices(self):
         m = build_model({}, "simple_supply,two_hours,investment_costs,shadow_prices")
         m.build()
         return m
 
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def simple_supply_yaml(self):
         m = build_model({}, "simple_supply,two_hours,investment_costs,shadow_prices")
         m.build()
         return m
 
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def simple_supply_yaml_invalid(self):
         m = build_model(
             {},
@@ -2518,7 +2411,7 @@ def simple_supply_yaml_invalid(self):
         m.build()
         return m
 
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def supply_milp_yaml(self):
         m = build_model({}, "supply_milp,two_hours,investment_costs,shadow_prices")
         m.build()
diff --git a/tests/test_backend_pyomo_objective.py b/tests/test_backend_pyomo_objective.py
index 43647f44..2427daa1 100644
--- a/tests/test_backend_pyomo_objective.py
+++ b/tests/test_backend_pyomo_objective.py
@@ -1,10 +1,11 @@
 import calliope
 import pyomo.core as po
 import pytest
-from pytest import approx
 
 from .common.util import build_test_model as build_model
 
+approx = pytest.approx
+
 
 @pytest.mark.skip(reason="to be reimplemented by comparison to LP files")
 class TestCostMinimisationObjective:
@@ -53,7 +54,7 @@ def test_nationalscale_maximize_utility(self):
         assert float(model.results.cost.sum()) > 6.6e7
 
     @pytest.mark.parametrize(
-        "scenario,cost_class,weight",
+        ("scenario", "cost_class", "weight"),
         [
             ("monetary_objective", ["monetary"], [1]),
             ("emissions_objective", ["emissions"], [1]),
diff --git a/tests/test_backend_where_parser.py b/tests/test_backend_where_parser.py
index ae8691ea..1e7ec946 100644
--- a/tests/test_backend_where_parser.py
+++ b/tests/test_backend_where_parser.py
@@ -17,50 +17,50 @@ def parse_yaml(yaml_string):
     return AttrDict.from_yaml_string(yaml_string)
 
 
-@pytest.fixture
+@pytest.fixture()
 def base_parser_elements():
     number, identifier = expression_parser.setup_base_parser_elements()
     return number, identifier
 
 
-@pytest.fixture
+@pytest.fixture()
 def number(base_parser_elements):
     return base_parser_elements[0]
 
 
-@pytest.fixture
+@pytest.fixture()
 def identifier(base_parser_elements):
     return base_parser_elements[1]
 
 
-@pytest.fixture
+@pytest.fixture()
 def data_var(identifier):
     return where_parser.data_var_parser(identifier)
 
 
-@pytest.fixture
+@pytest.fixture()
 def config_option(identifier):
     return where_parser.config_option_parser(identifier)
 
 
-@pytest.fixture
+@pytest.fixture()
 def bool_operand():
     return where_parser.bool_parser()
 
 
-@pytest.fixture
+@pytest.fixture()
 def evaluatable_string(identifier):
     return where_parser.evaluatable_string_parser(identifier)
 
 
-@pytest.fixture
+@pytest.fixture()
 def helper_function(number, identifier, evaluatable_string):
     return expression_parser.helper_function_parser(
         evaluatable_string, number, generic_identifier=identifier
     )
 
 
-@pytest.fixture
+@pytest.fixture()
 def comparison(
     evaluatable_string, number, helper_function, bool_operand, config_option, data_var
 ):
@@ -74,19 +74,19 @@ def comparison(
     )
 
 
-@pytest.fixture
+@pytest.fixture()
 def subset(identifier, evaluatable_string, number):
     return where_parser.subset_parser(identifier, evaluatable_string, number)
 
 
-@pytest.fixture
+@pytest.fixture()
 def where(bool_operand, helper_function, data_var, comparison, subset):
     return where_parser.where_parser(
         bool_operand, helper_function, data_var, comparison, subset
     )
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def eval_kwargs(dummy_pyomo_backend_model):
     return {
         "input_data": dummy_pyomo_backend_model.inputs,
@@ -98,7 +98,7 @@ def eval_kwargs(dummy_pyomo_backend_model):
     }
 
 
-@pytest.fixture
+@pytest.fixture()
 def parse_where_string(eval_kwargs, where):
     def _parse_where_string(where_string):
         parsed_ = where.parse_string(where_string, parse_all=True)
@@ -109,7 +109,7 @@ def _parse_where_string(where_string):
 
 class TestParserElements:
     @pytest.mark.parametrize(
-        ["data_var_string", "expected"],
+        ("data_var_string", "expected"),
         [("with_inf", "with_inf"), ("all_inf", "all_inf"), ("all_nan", "all_nan")],
     )
     def test_data_var(
@@ -124,7 +124,7 @@ def test_data_var(
         )
 
     @pytest.mark.parametrize(
-        ["data_var_string", "expected"],
+        ("data_var_string", "expected"),
         [
             ("with_inf", "with_inf_as_bool"),
             ("all_inf", "all_false"),
@@ -142,14 +142,13 @@ def test_data_var_with_where(
         )
 
     @pytest.mark.parametrize(
-        ["data_var_string", "expected_similar"],
+        ("data_var_string", "expected_similar"),
         [("multi_dim_var", "with_inf_as_bool"), ("multi_dim_expr", "all_true")],
     )
     def test_data_var_with_where_decision_variable_or_expr(
         self, data_var, dummy_model_data, data_var_string, expected_similar, eval_kwargs
     ):
-        """
-        Can't quite compare in the same way for decision variables / global expressions
+        """Can't quite compare in the same way for decision variables / global expressions
         as with params, because there is a random element to the `definition_matrix` array
         """
         parsed_ = data_var.parse_string(data_var_string, parse_all=True)
@@ -196,7 +195,7 @@ def test_data_var_fail_cannot_handle_constraint(self, data_var, eval_kwargs):
         )
 
     @pytest.mark.parametrize(
-        ["config_string", "expected_val"],
+        ("config_string", "expected_val"),
         [
             ("config.foo", True),
             ("config.FOO", "baz"),
@@ -240,7 +239,7 @@ def test_config_missing_from_data(self, config_option, eval_kwargs, config_strin
             parsed_[0].eval(**eval_kwargs)
 
     @pytest.mark.parametrize(
-        ["config_string", "type_"], [("config.b_a", "list"), ("config.bar", "AttrDict")]
+        ("config_string", "type_"), [("config.b_a", "list"), ("config.bar", "AttrDict")]
     )
     def test_config_fail_datatype(
         self, config_option, eval_kwargs, config_string, type_
@@ -253,7 +252,7 @@ def test_config_fail_datatype(
         )
 
     @pytest.mark.parametrize(
-        ["bool_string", "expected_true"],
+        ("bool_string", "expected_true"),
         [
             ("true", True),
             ("TRUE", True),
@@ -296,7 +295,7 @@ def test_evaluatable_string_parser_protected(self, evaluatable_string, instring)
         assert check_error_or_warning(excinfo, "Found unwanted token")
 
     @pytest.mark.parametrize(
-        ["var_string", "comparison_val", "n_true"],
+        ("var_string", "comparison_val", "n_true"),
         [
             ("all_inf", ".inf", 8),
             ("all_inf", 1, 0),
@@ -318,7 +317,7 @@ def test_comparison_parser_data_var(
         assert evaluated_.sum() == n_true
 
     @pytest.mark.parametrize(
-        ["operator", "comparison_val", "n_true"],
+        ("operator", "comparison_val", "n_true"),
         [("=", ".inf", 1), ("<", 3, 4), ("<=", 3, 5), (">", 1, 5), (">=", 1, 8)],
     )
     def test_comparison_parser_data_var_different_ops(
@@ -330,7 +329,7 @@ def test_comparison_parser_data_var_different_ops(
         assert evaluated_.sum() == n_true
 
     @pytest.mark.parametrize(
-        ["config_string", "comparison_val", "expected_true"],
+        ("config_string", "comparison_val", "expected_true"),
         [
             ("config.foo", "True", True),
             ("config.foo", "False", False),
@@ -367,7 +366,7 @@ def test_comparison_malformed_string(self, comparison, comparison_string):
         assert check_error_or_warning(excinfo, "Expected")
 
     @pytest.mark.parametrize(
-        ["subset_string", "expected_subset"],
+        ("subset_string", "expected_subset"),
         [
             ("[bar]", ["bar"]),
             ("[foo, bar]", ["foo", "bar"]),
@@ -398,7 +397,7 @@ def test_subsetting_parser_malformed(self, subset, subset_string):
         assert check_error_or_warning(excinfo, "Expected")
 
     @pytest.mark.parametrize(
-        ["parser_name", "parse_string", "expected"],
+        ("parser_name", "parse_string", "expected"),
         [
             ("data_var", "foo", "DATA_VAR:foo"),
             ("config_option", "config.bar", "CONFIG:bar"),
@@ -415,7 +414,7 @@ def test_repr(self, request, parser_name, parse_string, expected):
 
 class TestParserMasking:
     @pytest.mark.parametrize(
-        ["instring", "expected"],
+        ("instring", "expected"),
         [
             ("all_inf", "all_false"),
             ("config.foo=True", True),
@@ -432,7 +431,7 @@ def test_no_aggregation(
             assert evaluated_ == expected
 
     @pytest.mark.parametrize(
-        ["instring", "expected_true"],
+        ("instring", "expected_true"),
         [
             ("config.foo=True and config.a_b=0", True),
             ("config.foo=False And config.a_b=0", False),
@@ -446,7 +445,7 @@ def test_where_string_and(self, parse_where_string, instring, expected_true):
         assert evaluated_ if expected_true else not evaluated_
 
     @pytest.mark.parametrize(
-        ["instring", "expected_true"],
+        ("instring", "expected_true"),
         [
             ("config.foo=True or config.a_b=0", True),
             ("config.foo=False Or config.a_b=0", True),
@@ -460,7 +459,7 @@ def test_where_string_or(self, parse_where_string, instring, expected_true):
         assert evaluated_ if expected_true else not evaluated_
 
     @pytest.mark.parametrize(
-        ["instring", "expected_true"],
+        ("instring", "expected_true"),
         [
             ("not config.foo=True", False),
             ("Not config.foo=False and config.a_b=0", True),
@@ -474,7 +473,7 @@ def test_where_string_not(self, parse_where_string, instring, expected_true):
         assert evaluated_ if expected_true else not evaluated_
 
     @pytest.mark.parametrize(
-        ["instring", "expected"],
+        ("instring", "expected"),
         [
             ("all_inf and all_nan", "all_false"),
             ("all_inf or not all_nan", "all_true"),
@@ -495,7 +494,7 @@ def test_where_arrays(
         )
 
     @pytest.mark.parametrize(
-        ["instring", "expected"],
+        ("instring", "expected"),
         [
             ("[foo, bar] in nodes", "nodes_true"),
             (
@@ -523,7 +522,7 @@ def test_where_arrays_subsetting(
         assert evaluated_.equals(dummy_model_data[expected])
 
     @pytest.mark.parametrize(
-        ["instring", "expected"],
+        ("instring", "expected"),
         [
             ("all_inf and all_nan or config.foo=True", "all_true"),
             ("all_inf and (all_nan or config.foo=True)", "all_false"),
@@ -564,13 +563,13 @@ def test_where_malformed(self, where, instring):
 
 
 class TestAsMathString:
-    @pytest.fixture
+    @pytest.fixture()
     def latex_eval_kwargs(self, eval_kwargs):
         eval_kwargs["return_type"] = "math_string"
         return eval_kwargs
 
     @pytest.mark.parametrize(
-        ["parser", "instring", "expected"],
+        ("parser", "instring", "expected"),
         [
             ("data_var", "with_inf", r"\exists (\textit{with_inf}_\text{node,tech})"),
             ("data_var", "foo", r"\exists (\textit{foo})"),
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 66854728..67cc97ee 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -88,7 +88,7 @@ def test_incorrect_model_format(self):
         assert result.exit_code == 1
 
     @pytest.mark.parametrize(
-        "arg", (("--scenario=test"), ("--override_dict={'config.init.name': 'test'}"))
+        "arg", [("--scenario=test"), ("--override_dict={'config.init.name': 'test'}")]
     )
     def test_unavailable_arguments(self, arg):
         runner = CliRunner()
diff --git a/tests/test_constraint_results.py b/tests/test_constraint_results.py
index 6b7d0dc9..6fdd4012 100644
--- a/tests/test_constraint_results.py
+++ b/tests/test_constraint_results.py
@@ -1,9 +1,10 @@
 import calliope
 import pytest
-from pytest import approx
 
 from .common.util import build_test_model as build_model
 
+approx = pytest.approx
+
 
 @pytest.mark.skip(reason="to be reimplemented by comparison to LP files")
 class TestNationalScaleExampleModelSenseChecks:
@@ -102,7 +103,7 @@ def _get_flow(model, flow):
 
 @pytest.mark.skip(reason="to be reimplemented by comparison to LP files")
 class TestModelSettings:
-    @pytest.fixture
+    @pytest.fixture()
     def run_model(self):
         def _run_model(feasibility, cap_val):
             override_dict = {
@@ -130,15 +131,15 @@ def _run_model(feasibility, cap_val):
 
         return _run_model
 
-    @pytest.fixture
+    @pytest.fixture()
     def model_no_unmet(self, run_model):
         return run_model(True, 10)
 
-    @pytest.fixture
+    @pytest.fixture()
     def model_unmet_demand(self, run_model):
         return run_model(True, 5)
 
-    @pytest.fixture
+    @pytest.fixture()
     def model_unused_supply(self, run_model):
         return run_model(True, 15)
 
@@ -179,7 +180,7 @@ def test_expected_impact_on_objective_function_value(
             == approx(1e3 * 15)
         )
 
-    @pytest.mark.parametrize("override", (5, 15))
+    @pytest.mark.parametrize("override", [5, 15])
     def test_expected_infeasible_result(self, override, run_model):
         model = run_model(False, override)
 
@@ -190,7 +191,7 @@ def test_expected_infeasible_result(self, override, run_model):
 
 @pytest.mark.skip(reason="to be reimplemented by comparison to LP files")
 class TestEnergyCapacityPerStorageCapacity:
-    @pytest.fixture
+    @pytest.fixture()
     def model_file(self):
         return "flow_cap_per_storage_cap.yaml"
 
diff --git a/tests/test_core_attrdict.py b/tests/test_core_attrdict.py
index 095a0e5c..d36e7a89 100644
--- a/tests/test_core_attrdict.py
+++ b/tests/test_core_attrdict.py
@@ -11,7 +11,7 @@
 
 
 class TestAttrDict:
-    @pytest.fixture
+    @pytest.fixture()
     def regular_dict(self):
         d = {
             "a": 1,
@@ -37,16 +37,16 @@ def regular_dict(self):
         d:
     """
 
-    @pytest.fixture
+    @pytest.fixture()
     def yaml_filepath(self):
         this_path = Path(__file__).parent
         return this_path / "common" / "yaml_file.yaml"
 
-    @pytest.fixture
+    @pytest.fixture()
     def yaml_string(self):
         return self.setup_string
 
-    @pytest.fixture
+    @pytest.fixture()
     def attr_dict(self, regular_dict):
         d = regular_dict
         return AttrDict(d)
@@ -58,7 +58,7 @@ def test_missing_nonzero(self):
         assert _MISSING.__nonzero__() is False
 
     def test_init_from_nondict(self):
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError) as excinfo:  # noqa: PT011, false positive
             AttrDict("foo")
         assert check_error_or_warning(excinfo, "Must pass a dict to AttrDict")
 
@@ -93,7 +93,7 @@ def test_from_yaml_string_dot_strings_duplicate(self):
 
     def test_simple_invalid_yaml(self):
         yaml_string = "1 this is not valid yaml"
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError) as excinfo:  # noqa: PT011, false positive
             AttrDict.from_yaml_string(yaml_string)
         assert check_error_or_warning(excinfo, "Could not parse <yaml string> as YAML")
 
@@ -324,7 +324,7 @@ def test_import_must_be_list(self):
         yaml_string = """
             import: 'somefile.yaml'
         """
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError) as excinfo:  # noqa: PT011, false positive
             AttrDict.from_yaml_string(yaml_string, resolve_imports=True)
         assert check_error_or_warning(excinfo, "`import` must be a list.")
 
@@ -356,9 +356,7 @@ def test_nested_import(self):
                     baz: 2
                     3:
                         4: 5
-            """.format(
-                imported_file
-            )
+            """.format(imported_file)
 
             d = AttrDict.from_yaml_string(yaml_string, resolve_imports="foobar")
 
diff --git a/tests/test_core_model.py b/tests/test_core_model.py
index df86cdaa..5d0c3e87 100644
--- a/tests/test_core_model.py
+++ b/tests/test_core_model.py
@@ -65,14 +65,14 @@ def test_add_observed_dict_not_dict(self, national_scale_example):
 
 
 class TestAddMath:
-    @pytest.fixture
+    @pytest.fixture()
     def storage_inter_cluster(self):
         return build_model(
             {"config.init.add_math": ["storage_inter_cluster"]},
             "simple_supply,two_hours,investment_costs",
         )
 
-    @pytest.fixture
+    @pytest.fixture()
     def temp_path(self, tmpdir_factory):
         return tmpdir_factory.mktemp("custom_math")
 
@@ -86,7 +86,7 @@ def test_variable_bound(self, storage_inter_cluster):
         )
 
     @pytest.mark.parametrize(
-        ["override", "expected"],
+        ("override", "expected"),
         [
             (["foo"], ["foo"]),
             (["bar", "foo"], ["bar", "foo"]),
@@ -214,7 +214,7 @@ def test_base_math(self, caplog, simple_supply):
         ]
 
     @pytest.mark.parametrize(
-        ["equation", "where"],
+        ("equation", "where"),
         [
             ("1 == 1", "True"),
             (
diff --git a/tests/test_core_preprocess.py b/tests/test_core_preprocess.py
index 4a216677..5d7b83cc 100644
--- a/tests/test_core_preprocess.py
+++ b/tests/test_core_preprocess.py
@@ -12,9 +12,7 @@
 
 class TestModelRun:
     def test_model_from_dict(self, data_source_dir):
-        """
-        Test creating a model from dict/AttrDict instead of from YAML
-        """
+        """Test creating a model from dict/AttrDict instead of from YAML"""
         model_dir = data_source_dir.parent
         model_location = model_dir / "model.yaml"
         model_dict = AttrDict.from_yaml(model_location)
@@ -39,9 +37,7 @@ def test_model_from_dict(self, data_source_dir):
         "ignore:(?s).*(links, test_link_a_b_elec) | Deactivated:calliope.exceptions.ModelWarning"
     )
     def test_valid_scenarios(self):
-        """
-        Test that valid scenario definition from overrides raises no error and results in applied scenario.
-        """
+        """Test that valid scenario definition from overrides raises no error and results in applied scenario."""
         override = AttrDict.from_yaml_string(
             """
             scenarios:
@@ -67,8 +63,7 @@ def test_valid_scenarios(self):
         assert model._model_def_dict.techs.test_supply_elec.flow_cap_max == 20
 
     def test_valid_scenario_of_scenarios(self):
-        """
-        Test that valid scenario definition which groups scenarios and overrides raises
+        """Test that valid scenario definition which groups scenarios and overrides raises
         no error and results in applied scenario.
         """
         override = AttrDict.from_yaml_string(
@@ -100,9 +95,7 @@ def test_valid_scenario_of_scenarios(self):
         assert model._model_def_dict.techs.test_supply_elec.flow_cap_max == 20
 
     def test_invalid_scenarios_dict(self):
-        """
-        Test that invalid scenario definition raises appropriate error
-        """
+        """Test that invalid scenario definition raises appropriate error"""
         override = AttrDict.from_yaml_string(
             """
             scenarios:
@@ -118,9 +111,7 @@ def test_invalid_scenarios_dict(self):
         )
 
     def test_invalid_scenarios_str(self):
-        """
-        Test that invalid scenario definition raises appropriate error
-        """
+        """Test that invalid scenario definition raises appropriate error"""
         override = AttrDict.from_yaml_string(
             """
             scenarios:
@@ -135,9 +126,7 @@ def test_invalid_scenarios_str(self):
         )
 
     def test_undefined_carriers(self):
-        """
-        Test that user has input either carrier or carrier_in/_out for each tech
-        """
+        """Test that user has input either carrier or carrier_in/_out for each tech"""
         override = AttrDict.from_yaml_string(
             """
             techs:
@@ -153,8 +142,7 @@ def test_undefined_carriers(self):
             build_model(override_dict=override, scenario="simple_supply,one_day")
 
     def test_incorrect_subset_time(self):
-        """
-        If time_subset is a list, it must have two entries (start_time, end_time)
+        """If time_subset is a list, it must have two entries (start_time, end_time)
         If time_subset is not a list, it should successfully subset on the given
         string/integer
         """
@@ -210,8 +198,7 @@ def override(param):
             )
 
     def test_inconsistent_time_indices_fails(self):
-        """
-        Test that, including after any time subsetting, the indices of all time
+        """Test that, including after any time subsetting, the indices of all time
         varying input data are consistent with each other
         """
         # should fail: wrong length of demand_heat csv vs demand_elec
@@ -235,8 +222,7 @@ def test_inconsistent_time_indices_passes_thanks_to_time_subsetting(self):
             build_model(override_dict=override, scenario="simple_conversion,one_day")
 
     def test_single_timestep(self):
-        """
-        Test that warning is raised on using 1 timestep, that timestep resolution will
+        """Test that warning is raised on using 1 timestep, that timestep resolution will
         be inferred to be 1 hour
         """
         override1 = {
@@ -256,8 +242,7 @@ def test_single_timestep(self):
 class TestChecks:
     @pytest.mark.parametrize("top_level_key", ["init", "solve"])
     def test_unrecognised_config_keys(self, top_level_key):
-        """
-        Check that the only keys allowed in 'model' and 'run' are those in the
+        """Check that the only keys allowed in 'model' and 'run' are those in the
         model defaults
         """
         override = {f"config.{top_level_key}.nonsensical_key": "random_string"}
@@ -270,8 +255,7 @@ def test_unrecognised_config_keys(self, top_level_key):
         )
 
     def test_model_version_mismatch(self):
-        """
-        Model config says config.init.calliope_version = 0.1, which is not what we
+        """Model config says config.init.calliope_version = 0.1, which is not what we
         are running, so we want a warning.
         """
         override = {"config.init.calliope_version": "0.1"}
@@ -284,10 +268,7 @@ def test_model_version_mismatch(self):
         )
 
     def test_unspecified_base_tech(self):
-        """
-        All technologies and technology groups must specify a base_tech
-        """
-
+        """All technologies and technology groups must specify a base_tech"""
         override = AttrDict.from_yaml_string(
             """
             techs.test_supply_no_base_tech:
@@ -303,10 +284,7 @@ def test_unspecified_base_tech(self):
             build_model(override_dict=override, scenario="simple_supply,one_day")
 
     def test_tech_as_base_tech(self):
-        """
-        All technologies and technology groups must specify a base_tech
-        """
-
+        """All technologies and technology groups must specify a base_tech"""
         override1 = AttrDict.from_yaml_string(
             """
             techs.test_supply_tech_base_tech:
@@ -330,8 +308,7 @@ def test_tech_as_base_tech(self):
         reason="one_way doesn't work yet. We'll need to move this test to `model_data` once it does work."
     )
     def test_one_way(self):
-        """
-        With one_way transmission, we remove one direction of a link from
+        """With one_way transmission, we remove one direction of a link from
         loc_tech_carriers_prod and the other from loc_tech_carriers_con.
         """
         override = {
@@ -364,11 +341,9 @@ def test_one_way(self):
         reason="Removed this check because it has to happen *after* calling `build`"
     )
     def test_clustering_and_cyclic_storage(self):
-        """
-        Don't allow time clustering with cyclic storage if not also using
+        """Don't allow time clustering with cyclic storage if not also using
         storage_inter_cluster
         """
-
         override = {
             "config.init.time_subset": ["2005-01-01", "2005-01-04"],
             "config.init.time_cluster": "data_sources/cluster_days.csv",
@@ -384,11 +359,9 @@ def test_clustering_and_cyclic_storage(self):
         reason="Removed this check because it has to happen *after* calling `build`"
     )
     def test_storage_inter_cluster_vs_storage_discharge_depth(self):
-        """
-        Check that the storage_inter_cluster is not used together with storage_discharge_depth
-        """
+        """Check that the storage_inter_cluster is not used together with storage_discharge_depth"""
+        override = {"config.init.time_subset": ["2005-01-01", "2005-01-04"]}
         with pytest.raises(exceptions.ModelError) as error:
-            override = {"config.init.time_subset": ["2005-01-01", "2005-01-04"]}
             build_model(override, "clustering,simple_storage,storage_discharge_depth")
 
         assert check_error_or_warning(
diff --git a/tests/test_core_util.py b/tests/test_core_util.py
index b3d8f32c..e71c6e2c 100644
--- a/tests/test_core_util.py
+++ b/tests/test_core_util.py
@@ -22,7 +22,7 @@
 
 class TestLogging:
     @pytest.mark.parametrize(
-        ["level", "include_solver_output", "expected_level", "expected_solver_level"],
+        ("level", "include_solver_output", "expected_level", "expected_solver_level"),
         [
             ("CRITICAL", True, 50, 10),
             ("CRITICAL", False, 50, 50),
@@ -70,7 +70,7 @@ def test_timing_log(self):
         )
 
     @pytest.mark.parametrize(
-        ["capture", "expected_level", "n_handlers"], [(True, 20, 1), (False, 30, 0)]
+        ("capture", "expected_level", "n_handlers"), [(True, 20, 1), (False, 30, 0)]
     )
     def test_capture_warnings(self, capture, expected_level, n_handlers):
         calliope.set_log_verbosity("info", capture_warnings=capture)
@@ -130,7 +130,7 @@ class TestValidateDict:
         reason="Checking the schema itself doesn't seem to be working properly; no clear idea of _why_ yet..."
     )
     @pytest.mark.parametrize(
-        ["schema_dict", "expected_path"],
+        ("schema_dict", "expected_path"),
         [
             ({"foo": 2}, ""),
             ({"properties": {"bar": {"foo": "string"}}}, " at `properties.bar`"),
@@ -153,7 +153,7 @@ def test_malformed_schema(self, schema_dict, expected_path):
         )
 
     @pytest.mark.parametrize(
-        ["to_validate", "expected_path"],
+        ("to_validate", "expected_path"),
         [
             ({"invalid": {"foo": 2}}, ""),
             ({"valid": {"foo": 2, "invalid": 3}}, "valid: "),
@@ -180,7 +180,7 @@ def test_invalid_dict(self, to_validate, expected_path):
             ],
         )
 
-    @pytest.fixture
+    @pytest.fixture()
     def base_math(self):
         return calliope.AttrDict.from_yaml(
             Path(calliope.__file__).parent / "math" / "base.yaml"
@@ -320,7 +320,7 @@ def sample_model_def_schema(self):
         """
         return calliope.AttrDict.from_yaml_string(schema_string)
 
-    @pytest.fixture
+    @pytest.fixture()
     def expected_config_defaults(self):
         return pd.Series(
             {
@@ -330,7 +330,7 @@ def expected_config_defaults(self):
             }
         ).sort_index()
 
-    @pytest.fixture
+    @pytest.fixture()
     def expected_model_def_defaults(self):
         return pd.Series(
             {
@@ -364,7 +364,7 @@ def test_extract_model_def_defaults(
         )
 
     @pytest.mark.parametrize(
-        ["schema_key", "prop_keys"],
+        ("schema_key", "prop_keys"),
         [
             ("parameters", ["objective_cost_weights"]),
             ("nodes", ["available_area"]),
@@ -397,7 +397,6 @@ def test_extract_defaults_subset(
 
 
 class TestUpdateSchema:
-
     @pytest.mark.parametrize("top_level", ["parameters", "nodes", "techs"])
     def test_add_new_schema(self, top_level):
         schema.update_model_schema(
diff --git a/tests/test_example_models.py b/tests/test_example_models.py
index ed09e9d4..1983c2ee 100755
--- a/tests/test_example_models.py
+++ b/tests/test_example_models.py
@@ -6,16 +6,17 @@
 import pandas as pd
 import pytest
 from calliope import exceptions
-from pytest import approx
 
 from .common.util import check_error_or_warning
 
+approx = pytest.approx
+
 
 class TestModelPreproccessing:
     def test_preprocess_national_scale(self):
         calliope.examples.national_scale()
 
-    @pytest.mark.time_intensive
+    @pytest.mark.time_intensive()
     def test_preprocess_time_clustering(self):
         calliope.examples.time_clustering()
 
@@ -227,7 +228,7 @@ def test_nationalscale_example_results_gurobi(self):
         assert np.allclose(gurobi_data.flow_cap, gurobi_persistent_data.flow_cap)
         assert np.allclose(gurobi_data.cost, gurobi_persistent_data.cost)
 
-    @pytest.fixture
+    @pytest.fixture()
     def base_model_data(self):
         model = calliope.examples.national_scale(
             time_subset=["2005-01-01", "2005-01-03"], scenario="spores"
@@ -238,7 +239,7 @@ def base_model_data(self):
 
         return model._model_data
 
-    @pytest.mark.parametrize("init_spore", (0, 1, 2))
+    @pytest.mark.parametrize("init_spore", [0, 1, 2])
     def test_nationalscale_skip_cost_op_spores(self, base_model_data, init_spore):
         spores_model = calliope.Model(
             config=None, model_data=base_model_data.loc[{"spores": [init_spore + 1]}]
@@ -259,14 +260,14 @@ def test_fail_with_spores_as_input_dim(self, base_model_data):
         spores_model = calliope.Model(
             config=None, model_data=base_model_data.loc[{"spores": [0, 1]}]
         )
+        spores_model.build()
         with pytest.raises(exceptions.ModelError) as excinfo:
-            spores_model.build()
             spores_model.solve(force=True)
         assert check_error_or_warning(
             excinfo, "Cannot run SPORES with a SPORES dimension in any input"
         )
 
-    @pytest.fixture
+    @pytest.fixture()
     def spores_with_override(self):
         def _spores_with_override(override_dict):
             result_without_override = self.example_tester()
diff --git a/tests/test_io.py b/tests/test_io.py
index efa4e5cc..8caccdeb 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -59,7 +59,7 @@ def test_save_netcdf(self, model_file):
         assert os.path.isfile(model_file)
 
     @pytest.mark.parametrize(
-        ["attr", "expected_type", "expected_val"],
+        ("attr", "expected_type", "expected_val"),
         [
             ("foo_true", bool, True),
             ("foo_false", bool, False),
@@ -92,7 +92,7 @@ def test_serialised_list_popped(self, request, serialised_list, model_name):
         assert serialised_list not in model._model_data.attrs.keys()
 
     @pytest.mark.parametrize(
-        ["serialisation_list_name", "list_elements"],
+        ("serialisation_list_name", "list_elements"),
         [
             ("serialised_bools", ["foo_true", "foo_false"]),
             ("serialised_nones", ["foo_none", "scenario"]),
diff --git a/tests/test_math.py b/tests/test_math.py
index bd382416..90c42c01 100644
--- a/tests/test_math.py
+++ b/tests/test_math.py
@@ -123,7 +123,7 @@ def test_source_max(self, compare_lps):
         compare_lps(model, custom_math, "source_max")
 
     def test_balance_transmission(self, compare_lps):
-        "Test with the electricity transmission tech being one way only, while the heat transmission tech is the default two-way."
+        """Test with the electricity transmission tech being one way only, while the heat transmission tech is the default two-way."""
         self.TEST_REGISTER.add("constraints.balance_transmission")
         model = build_test_model(
             {"techs.test_link_a_b_elec.one_way": True}, "simple_conversion,two_hours"
@@ -136,7 +136,7 @@ def test_balance_transmission(self, compare_lps):
         compare_lps(model, custom_math, "balance_transmission")
 
     def test_balance_storage(self, compare_lps):
-        "Test balance storage with one tech having and one tech not having per-tech cyclic storage."
+        """Test balance storage with one tech having and one tech not having per-tech cyclic storage."""
         self.TEST_REGISTER.add("constraints.balance_storage")
         model = build_test_model(
             {
@@ -152,7 +152,7 @@ def test_balance_storage(self, compare_lps):
 
     @pytest.mark.xfail(reason="not all base math is in the test config dict yet")
     def test_all_math_registered(self, base_math):
-        "After running all the previous tests in the class, the base_math dict should be empty, i.e. all math has been tested"
+        """After running all the previous tests in the class, the base_math dict should be empty, i.e. all math has been tested"""
         for key in self.TEST_REGISTER:
             base_math.del_key(key)
         assert not base_math
@@ -169,7 +169,7 @@ class CustomMathExamples(ABC):
     @property
     @abstractmethod
     def YAML_FILEPATH(self) -> str:
-        "Source of the specific test class custom math"
+        """Source of the specific test class custom math"""
 
     @pytest.fixture(scope="class")
     def abs_filepath(self):
@@ -179,7 +179,7 @@ def abs_filepath(self):
     def custom_math(self):
         return AttrDict.from_yaml(self.CUSTOM_MATH_DIR / self.YAML_FILEPATH)
 
-    @pytest.fixture
+    @pytest.fixture()
     def build_and_compare(self, abs_filepath, compare_lps):
         def _build_and_compare(
             filename: str,
@@ -214,7 +214,7 @@ def _build_and_compare(
 
     @pytest.mark.order(-1)
     def test_all_math_registered(self, custom_math):
-        "After running all the previous tests in the class, the register should be full, i.e. all math has been tested"
+        """After running all the previous tests in the class, the register should be full, i.e. all math has been tested"""
         for key in self.TEST_REGISTER:
             if custom_math.get_key(key, default=None) is not None:
                 custom_math.del_key(key)
diff --git a/tests/test_preprocess_data_sources.py b/tests/test_preprocess_data_sources.py
index 888c53ca..a612e7ce 100644
--- a/tests/test_preprocess_data_sources.py
+++ b/tests/test_preprocess_data_sources.py
@@ -60,7 +60,7 @@ def test_log_message(self, caplog, data_dir, source_obj):
         assert "(data_sources, ds_name) | bar." in caplog.text
 
     @pytest.mark.parametrize(
-        ["key", "expected"],
+        ("key", "expected"),
         [("rows", ["test_row"]), ("columns", None), ("foo", ["foobar"])],
     )
     def test_listify_if_defined(self, source_obj, key, expected):
@@ -71,7 +71,7 @@ def test_listify_if_defined(self, source_obj, key, expected):
             assert output == expected
 
     @pytest.mark.parametrize(
-        ["loaded", "defined"],
+        ("loaded", "defined"),
         [
             (["foo"], ["foo"]),
             ([None], ["foo"]),
@@ -84,7 +84,7 @@ def test_compare_axis_names_passes(self, source_obj, loaded, defined):
         source_obj._compare_axis_names(loaded, defined, "foobar")
 
     @pytest.mark.parametrize(
-        ["loaded", "defined"],
+        ("loaded", "defined"),
         [
             (["bar"], ["foo"]),
             ([None, "foo"], ["foo", "bar"]),
@@ -442,7 +442,7 @@ def source_obj(self, init_config):
         return ds
 
     @pytest.mark.parametrize(
-        ["param", "expected"],
+        ("param", "expected"),
         [
             ("FOO", {"foo1": {"FOO": ["bar1", "bar2"]}}),
             ("BAR", {"foo1": {"BAR": "bar1"}, "foo2": {"BAR": "bar2"}}),
diff --git a/tests/test_preprocess_model_data.py b/tests/test_preprocess_model_data.py
index d342b676..64d57a3c 100644
--- a/tests/test_preprocess_model_data.py
+++ b/tests/test_preprocess_model_data.py
@@ -14,7 +14,7 @@
 from .common.util import check_error_or_warning
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def model_def():
     filepath = Path(__file__).parent / "common" / "test_model" / "model.yaml"
     model_def_dict, model_def_path, _ = load.load_model_definition(
@@ -23,7 +23,7 @@ def model_def():
     return model_def_dict, model_def_path
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def data_source_list(model_def, init_config):
     model_def_dict, model_def_path = model_def
     return [
@@ -34,14 +34,14 @@ def data_source_list(model_def, init_config):
     ]
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def init_config(config_defaults, model_def):
     model_def_dict, _ = model_def
     config_defaults.union(model_def_dict.pop("config"), allow_override=True)
     return config_defaults["init"]
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def model_data_factory(model_def, init_config, model_defaults):
     model_def_dict, _ = model_def
     return ModelDataFactory(
@@ -49,13 +49,13 @@ def model_data_factory(model_def, init_config, model_defaults):
     )
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def model_data_factory_w_params(model_data_factory: ModelDataFactory):
     model_data_factory.add_node_tech_data()
     return model_data_factory
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture()
 def my_caplog(caplog):
     caplog.set_level(logging.DEBUG, logger="calliope.preprocess")
     return caplog
@@ -178,7 +178,7 @@ def test_clean_data_from_undefined_members(
         assert model_data_factory.dataset["definition_matrix"].dtype.kind == "b"
 
     @pytest.mark.parametrize(
-        ["existing_distance", "expected_distance"], [(np.nan, 343.834), (1, 1)]
+        ("existing_distance", "expected_distance"), [(np.nan, 343.834), (1, 1)]
     )
     def test_add_link_distances_missing_distance(
         self,
@@ -210,7 +210,7 @@ def test_add_link_distances_missing_distance(
             techs="test_link_a_b_elec"
         ).item() == pytest.approx(expected_distance)
 
-    @pytest.mark.parametrize(["unit", "expected"], [("m", 343834), ("km", 343.834)])
+    @pytest.mark.parametrize(("unit", "expected"), [("m", 343834), ("km", 343.834)])
     def test_add_link_distances_no_da(
         self, my_caplog, model_data_factory_w_params: ModelDataFactory, unit, expected
     ):
@@ -355,7 +355,7 @@ def test_get_relevant_node_refs_parent_at_node_not_supported(
         )
 
     @pytest.mark.parametrize(
-        ["param_data", "expected_da"],
+        ("param_data", "expected_da"),
         [
             ({"data": 1, "index": [[]], "dims": []}, xr.DataArray(1)),
             (
@@ -390,7 +390,7 @@ def test_definition_dict_to_ds(self, model_data_factory: ModelDataFactory):
         assert param_ds.broadcast_equals(expected_ds)
 
     @pytest.mark.parametrize(
-        ["input_idx", "expected_idx"],
+        ("input_idx", "expected_idx"),
         [
             ("foo", [["foo"]]),
             (["foo"], [["foo"]]),
@@ -409,7 +409,7 @@ def test_prepare_param_dict_indexed_idx(
         assert output == {"index": expected_idx, **dict_skeleton}
 
     @pytest.mark.parametrize(
-        ["input_dim", "expected_dim"],
+        ("input_dim", "expected_dim"),
         [("foo", ["foo"]), (["foo"], ["foo"]), (["foo", "bar"], ["foo", "bar"])],
     )
     def test_prepare_param_dict_indexed_dim(
@@ -434,7 +434,7 @@ def test_prepare_param_dict_lookup(
         assert output == {"data": True, "index": [["foo"], ["bar"]], "dims": ["foobar"]}
 
     def test_prepare_param_dict_not_lookup(self, model_data_factory: ModelDataFactory):
-        with pytest.raises(ValueError) as excinfo:
+        with pytest.raises(ValueError) as excinfo:  # noqa: PT011, false positive
             model_data_factory._prepare_param_dict("foo", ["foo", "bar"])
         assert check_error_or_warning(
             excinfo,
@@ -523,7 +523,7 @@ def test_inherit_defs_techs_missing_base_def(
         )
 
     @pytest.mark.parametrize(
-        ["node_dict", "expected_dict", "expected_inheritance"],
+        ("node_dict", "expected_dict", "expected_inheritance"),
         [
             ({"my_param": 1}, {"my_param": 1}, None),
             (
@@ -708,7 +708,7 @@ def test_add_to_dataset_no_timeseries(
         assert model_data_factory.dataset["non_ts_data"].equals(simple_da)
 
     @pytest.mark.parametrize(
-        ["data", "kind"],
+        ("data", "kind"),
         [
             ([1, 2], "i"),
             (["1", "2"], "i"),
@@ -733,7 +733,7 @@ def test_update_numeric_dims(
         )
         assert updated_ds.coords["bar"].dtype.kind == kind
 
-    @pytest.mark.parametrize(["data", "kind"], [(["1", 2], "i"), ([1.0, "2.0"], "f")])
+    @pytest.mark.parametrize(("data", "kind"), [(["1", 2], "i"), ([1.0, "2.0"], "f")])
     def test_update_numeric_dims_in_model_data(
         self, my_caplog, model_data_factory: ModelDataFactory, data, kind
     ):
@@ -764,7 +764,7 @@ def test_update_numeric_dims_no_update(
         assert updated_ds.coords["bar"].dtype.kind not in ["f", "i"]
 
     @pytest.mark.parametrize(
-        ["coords", "new_coords"],
+        ("coords", "new_coords"),
         [(["foobar", "baz"], ["baz"]), (["bazfoo", "baz"], ["bazfoo", "baz"])],
     )
     def test_log_param_updates_new_coord(
@@ -785,7 +785,7 @@ def test_log_param_updates_new_coord(
             )
 
     @pytest.mark.parametrize(
-        ["index", "new_items"],
+        ("index", "new_items"),
         [
             (("hello", 10), [("foobar", "hello")]),
             (("hello", 30), [("foobar", "hello"), ("foobaz", 30)]),
@@ -844,7 +844,7 @@ def test_raise_error_on_transmission_tech_in_node(
 
 
 class TestTopLevelParams:
-    @pytest.fixture(scope="function")
+    @pytest.fixture()
     def run_and_test(self, model_data_factory_w_params):
         def _run_and_test(in_dict, out_dict, dims):
             model_data_factory_w_params.model_definition["parameters"] = {
@@ -1000,8 +1000,7 @@ def test_top_level_param_extend_dim_vals(
 
 
 class TestActiveFalse:
-    """
-    Test removal of techs, nodes, links, and transmission techs
+    """Test removal of techs, nodes, links, and transmission techs
     with the ``active: False`` configuration option.
 
     """
diff --git a/tests/test_preprocess_time.py b/tests/test_preprocess_time.py
index 167126d6..87ddbfeb 100644
--- a/tests/test_preprocess_time.py
+++ b/tests/test_preprocess_time.py
@@ -6,7 +6,6 @@
 
 
 class TestTimeFormat:
-
     def test_change_date_format(self):
         """
         Test the date parser catches a different date format from file than