diff --git a/.gitignore b/.gitignore
index 0687299..13eb353 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 .cache/
+poetry.toml
 **/__pycache__/
 .venv/
diff --git a/README.md b/README.md
index 3892c23..12abf56 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,9 @@ The goal of the `lalib` project is to create
     by reading and writing code.
 
 
+[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+
+
 ## Contributing & Development
 
 This project is open for any kind of contribution,
@@ -65,6 +68,21 @@ To execute all default tasks, simply invoke:
 `nox`
 
 
+#### Code Formatting
+
+We follow [Google's Python style guide](https://google.github.io/styleguide/pyguide.html).
+
+During development,
+    `nox -s format` may be helpful.
+It can be speed up by re-using a previously created environment
+    with the `-R` flag.
+
+This task formats all source code files with
+    [autoflake](https://pypi.org/project/autoflake/),
+    [black](https://pypi.org/project/black/), and
+    [isort](https://pypi.org/project/isort/).
+
+
 ### Branching Strategy
 
 The branches in this repository follow the
diff --git a/noxfile.py b/noxfile.py
index 64333e7..087f97a 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,6 +1,9 @@
 """Maintenance tasks run in isolated environments."""
 
 import collections
+import pathlib
+import re
+import tempfile
 from collections.abc import Mapping
 from typing import Any
 
@@ -12,6 +15,9 @@ try:
     from nox_poetry import session as nox_session
 except ImportError:
     nox_session = nox.session
+    nox_poetry_available = False
+else:
+    nox_poetry_available = True
 
 
 def nested_defaultdict() -> collections.defaultdict[str, Any]:
@@ -67,10 +73,30 @@ nox.options.envdir = ".cache/nox"
 nox.options.error_on_external_run = True  # only `git` and `poetry` are external
 nox.options.reuse_venv = "no"
 nox.options.sessions = (  # run by default when invoking `nox` on the CLI
+    "format",
 )
 nox.options.stop_on_first_error = True
 
 
+@nox_session(name="format", python=MAIN_PYTHON)
+def format_(session: nox.Session) -> None:
+    """Format source files with `autoflake`, `black`, and `isort`."""
+    start(session)
+
+    install_pinned(session, "autoflake", "black", "isort")
+
+    locations = session.posargs or SRC_LOCATIONS
+
+    session.run("autoflake", "--version")
+    session.run("autoflake", *locations)
+
+    session.run("black", "--version")
+    session.run("black", *locations)
+
+    session.run("isort", "--version-number")
+    session.run("isort", *locations)
+
+
 def start(session: nox.Session) -> None:
     """Show generic info about a session."""
     if session.posargs:
@@ -83,10 +109,63 @@ def start(session: nox.Session) -> None:
     session.run("python", "-c", "import os; print(os.getcwd())")
     session.run("python", "-c", 'import os; print(os.environ["PATH"])')
 
+    session.env["BLACK_CACHE_DIR"] = ".cache/black"
     session.env["PIP_CACHE_DIR"] = ".cache/pip"
     session.env["PIP_DISABLE_PIP_VERSION_CHECK"] = "true"
 
 
+def install_pinned(
+    session: nox.Session,
+    *packages_or_pip_args: str,
+    **kwargs: Any,
+) -> None:
+    """Install packages respecting the "poetry.lock" file.
+
+    Wraps `nox.sessions.Session.install()` such that it installs
+    packages respecting the pinned versions specified in poetry's
+    lock file. This makes nox sessions more deterministic.
+    """
+    session.debug("Install packages respecting the poetry.lock file")
+
+    session.run(  # temporary fix to avoid poetry's future warning
+        "poetry",
+        "config",
+        "--local",
+        "warnings.export",
+        "false",
+        external=True,
+        log=False,  # because it's just a fix
+    )
+
+    if nox_poetry_available:
+        session.install(*packages_or_pip_args, **kwargs)
+        return
+
+    with tempfile.NamedTemporaryFile() as requirements_txt:
+        session.run(
+            "poetry",
+            "export",
+            "--format=requirements.txt",
+            f"--output={requirements_txt.name}",
+            "--with=dev",
+            "--without-hashes",
+            external=True,
+        )
+
+        # `pip install --constraint ...` raises an error if the
+        # dependencies in requirements.txt contain "extras"
+        # => Strip "package[extras]==1.2.3" into "package==1.2.3"
+        dependencies = pathlib.Path(requirements_txt.name).read_text().split("\n")
+        dependencies = [re.sub(r"\[.*\]==", "==", dep) for dep in dependencies]
+        pathlib.Path(requirements_txt.name).write_text("\n".join(dependencies))
+
+        session.install(
+            f"--constraint={requirements_txt.name}",
+            *packages_or_pip_args,
+            **kwargs,
+        )
+
+
 if MAIN_PYTHON not in SUPPORTED_PYTHONS:
     msg = f"MAIN_PYTHON version, v{MAIN_PYTHON}, is not in SUPPORTED_PYTHONS"
     raise RuntimeError(msg)
diff --git a/poetry.lock b/poetry.lock
index 2f7ba0f..a5994bb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,6 +1,186 @@
-package = []
+[[package]]
+name = "autoflake"
+version = "2.3.1"
+description = "Removes unused imports and unused variables"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840"},
+    {file = "autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e"},
+]
+
+[package.dependencies]
+pyflakes = ">=3.0.0"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "black"
+version = "24.8.0"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
+    {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
+    {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"},
+    {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"},
+    {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"},
+    {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"},
+    {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"},
+    {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"},
+    {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"},
+    {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"},
+    {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"},
+    {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"},
+    {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"},
+    {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"},
+    {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"},
+    {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"},
+    {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"},
+    {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"},
+    {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"},
+    {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"},
+    {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"},
+    {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "isort"
+version = "5.13.2"
+description = "A Python utility / library to sort Python imports."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+    {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
+    {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+    {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.1"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
+    {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+    {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"},
+    {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"},
+]
+
+[package.extras]
+docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
+type = ["mypy (>=1.11.2)"]
+
+[[package]]
+name = "pyflakes"
+version = "3.2.0"
+description = "passive checker of Python programs"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
+    {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "0ef693f84017d54b0ea1d26b405d979fc57973167b307ccd9ed780ad9bdc1a3a"
+content-hash = "01960a0fd26ee6cb565391f829bae290abbf990c15496a4c93b323b0253a09fa"
diff --git a/pyproject.toml b/pyproject.toml
index b4894b6..4684745 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,6 +32,10 @@ python = "^3.9"
 
 [tool.poetry.group.dev.dependencies]
 
+# Code formatters
+autoflake = "^2.3"
+black = "^24.8"
+isort = "^5.13"
 
 
 [tool.poetry.urls]
@@ -40,6 +44,57 @@ python = "^3.9"
 
 
 
+[tool.autoflake]
+# Source: https://github.com/PyCQA/autoflake#configuration
+
+in-place = true
+recursive = true
+expand-star-imports = true
+remove-all-unused-imports = true
+ignore-init-module-imports = true  # modifies "remove-all-unused-imports"
+remove-duplicate-keys = true
+remove-unused-variables = true
+
+
+
+[tool.black]
+# Source: https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html
+
+line-length = 88
+target-version = ["py312", "py311", "py310", "py39"]
+
+
+
+[tool.isort]
+# Source: https://pycqa.github.io/isort/docs/configuration/options.html
+
+known_first_party = ["lalib"]
+
+atomic = true
+case_sensitive = true
+combine_star = true
+force_alphabetical_sort_within_sections = true
+lines_after_imports = 2
+remove_redundant_aliases = true
+
+# Comply with black's style => Instead of: 'profile = "black"'
+# Source: https://pycqa.github.io/isort/docs/configuration/profiles.html
+ensure_newline_before_comments = true
+force_grid_wrap = 0
+include_trailing_comma = true
+line_length = 88
+multi_line_output = 3
+split_on_trailing_comma = true
+use_parentheses = true
+
+# Comply with Google's Python style guide
+# => All imports go on a single line (with some exceptions)
+# Source: https://google.github.io/styleguide/pyguide.html#313-imports-formatting
+force_single_line = true
+single_line_exclusions = ["collections.abc", "typing"]
+
+
+
 [build-system]
 
 requires = ["poetry-core"]