Quality Assurance: Testing and Linting
Last updated on 2026-02-11 | Edit this page
Estimated time: 30 minutes
Overview
Questions
- How do I keep development tools separate from my library dependencies?
- How can I automatically fix style errors?
- How do I ensure my code works as expected?
- What are pre-commit hooks?
Objectives
- Use
uv add --devto install tools for developers (linting, testing). - Configure
ruffto format code and catch bugs. - Write and run a simple test suite with
pytest. - Automate checks using
prek.
The “Works on My Machine” Problem (Again)
We have a pyproject.toml that defines
what our package needs to run (e.g., numpy).
But as developers, we need more tools. We need tools to:
- Format code (so it looks professional).
- Lint code (to catch bugs before running).
- Test code (to verify correctness).
We don’t want to force our users to install these tools just to use our library. We need Development Dependencies.
Development Dependencies with uv
We will use uv to add tools to a
special dev group. This keeps them
separate from the main dependencies.
BASH
# Add Ruff (linter), Pytest (testing), and plugins for coverage/randomization
uv add --dev ruff pytest pytest-cov pytest-randomly
This updates pyproject.toml:

Linting and Formatting with ruff
Ruff is an
extremely fast static analysis tool that replaces older tools like
flake8 (linting), black (formatting), and isort (sorting imports).
Let’s see how messy our code is. Open src/chemlib/geometry.py and make it “ugly”: add
some unused imports or bad spacing.
PYTHON
# src/chemlib/geometry.py
import os # Unused import!
import numpy as np
def center_of_mass(atoms):
x = 1 # Unused variable!
print("Calculating...")
data = np.array(atoms)
return np.mean(data, axis=0)
Now, run the linter:
src/chemlib/geometry.py:2:8: F401 [*] =os= imported but unused
src/chemlib/geometry.py:6:5: F841 [*] Local variable =x= is assigned to but never used
Found 2 errors.
ruff found code-smell. Now let’s fix
the formatting automatically:
Your code is now perfectly spaced and sorted according to community standards.
Testing with pytest
Now that the code looks right, does it work right?
We need to write a test. By convention, tests live in a tests/ directory at the root of your
project.
BASH
mkdir tests
# Create __init__.py to allow relative imports within the tests directory
touch tests/__init__.py
Create a test file tests/test_geometry.py:
SH
import numpy as np
import pytest
from chemlib.geometry import center_of_mass
def test_center_of_mass_simple():
"""Test COM of a simple diatomic molecule."""
atoms = [[0, 0, 0], [2, 0, 0]]
expected = [1.0, 0.0, 0.0]
result = center_of_mass(atoms)
# Use numpy's assertion helper for float comparisons
np.testing.assert_allclose(result, expected)
def test_center_of_mass_cube():
"""Test COM of a unit cube."""
atoms = [
[0,0,0], [1,0,0], [0,1,0], [0,0,1],
[1,1,0], [1,0,1], [0,1,1], [1,1,1]
]
expected = [0.5, 0.5, 0.5]
result = center_of_mass(atoms)
np.testing.assert_allclose(result, expected)
Run the tests using uv run. We include
the --cov flag (from pytest-cov) to see which lines of code our tests
executed:
tests/test_geometry.py .. [100%]
---------- coverage: platform linux, python 3.14 ----------
Name Stmts Miss Cover
-----------------------------------------------
src/chemlib/__init__.py 0 0 100%
src/chemlib/geometry.py 4 0 100%
-----------------------------------------------
TOTAL 4 0 100%
========================== 2 passed in 0.04s ==========================
Why did this work?
Because of the Src Layout established earlier, uv run installs the package in editable mode.
pytest imports chemlib as if it existed as a standard installed
library.
Challenge: Break the Test
Modify src/chemlib/geometry.py to
introduce a bug (e.g., divide by len(data) - 1 instead of the true mean).
- Run
uv run pytest. What happens? - Run
uv run ruff check. Does the linter catch this logic error?
-
Pytest Fails: It will show exactly where the
numbers mismatch (
AssertionError). - Ruff Passes: Linters check syntax and style, not logic. This is why we need both!
Automating best practices

We have tools, but we have to remember to run them.
pre-commit hooks automate this by running checks
before you can commit code. We will use prek, a
Rust rewrite of the venerable pre-commit.
First, add it to our dev tools:
Create a configuration file .pre-commit-config.yaml in the root
directory:
SH
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.0
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format
Now, run the hook:
.. and finally install the action as a git hook
prek installed at .git/hooks/pre-commit
Now, try to commit messy code. git will
stop you, run ruff, fix the file, and ask
you to stage the clean file. You can no longer commit ugly code by
accident!
-
Development Dependencies (
uv add --dev) keep tools like linters separate from library requirements. - Ruff is the modern standard for fast Python linting and formatting.
- Pytest verifies code correctness; Src Layout makes test discovery reliable.
- Pre-commit hooks ensure no bad code ever enters your version control history.