On this page
Show commands for

This guide picks up where Lab computing setup leaves off. It assumes you already have a terminal, Git, GitHub, and an editor working.

What follows is how the lab starts and runs a Python project, so that a year from now someone else — very possibly you — can clone it and reproduce what you did.

Why uv

Python’s oldest source of pain is that installing packages and managing interpreters are two separate problems solved by a dozen half-overlapping tools. uv collapses all of it into one program: it installs Python versions, creates virtual environments, resolves and locks dependencies, and runs your code. It is also fast enough that you stop thinking about it.

If you have used python -m venv, pip, pip-tools, pyenv, or conda, uv replaces all of them for a project like ours. There is a section at the end on working with older projects that still use pip.

Install uv and a Python

Windows

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

macOS

curl -LsSf https://astral.sh/uv/install.sh | sh

Linux

curl -LsSf https://astral.sh/uv/install.sh | sh

Close and reopen your terminal, then:

uv --version

Now install a Python. uv manages these itself — you do not need one from python.org:

uv python install 3.13

Start a project

uv init assay-analysis
cd assay-analysis

That gives you:

File What it is
pyproject.toml The project’s definition — its name, its Python version, its dependencies. The one file that matters most.
.python-version Which Python this project uses, so everyone gets the same one.
README.md Explain what the project does. Actually write this one.
.gitignore Files Git should never track.
main.py A placeholder entry point.

Two files you will see shortly but that do not exist yet: .venv/ (the project’s private Python environment) and uv.lock (the exact resolved version of every dependency). Both are created the first time you run uv add, uv sync, or uv run — not by uv init.

Add dependencies

uv add numpy pandas matplotlib

This resolves the versions, writes them into pyproject.toml, records the exact resolution in uv.lock, and installs everything into .venv/. There is no separate “create the environment” or “activate the environment” step — uv handles it.

Development-only tools go in a separate group so they do not ship with the project:

uv add --dev pytest ruff

To remove something, uv remove pandas. To install exactly what the lockfile says — which is what you do after cloning someone else’s project — uv sync.

Project layout

For anything beyond a single script, put your code in src/:

assay-analysis/
├── .venv/                     ← not in Git
├── .gitignore
├── pyproject.toml
├── uv.lock                    ← in Git
├── README.md
├── data/
│   └── raw/                   ← not in Git (see "Data" below)
├── notebooks/
│   └── 01-explore.ipynb
├── src/
│   └── assay_analysis/
│       ├── __init__.py
│       ├── io.py              ← reading plate-reader exports
│       ├── normalize.py       ← blank subtraction, plate normalization
│       └── fitting.py         ← dose–response curve fitting
└── tests/
    └── test_normalize.py

The reason for src/ rather than putting the package at the top level is that it forces your code to be installed to be importable, which means your tests exercise the package the same way a user would. It catches a whole class of “works on my machine” bug where a module only imports because you happened to be standing in the right directory.

Tell uv the package lives there by adding this to pyproject.toml:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/assay_analysis"]

Keep modules small and named after what they do. normalize.py holding the normalization logic is worth ten times a utils.py holding everything.

Running your code

Prefix commands with uv run and the project environment is used automatically:

uv run python -m assay_analysis
uv run python scripts/fit_plate.py

You never have to remember whether you activated an environment, because you never activate one. If a command works with uv run in front of it, it is using the right Python and the right packages.

Give the things you run often a name in pyproject.toml:

[project.scripts]
fit-plate = "assay_analysis.fitting:main"

after which uv run fit-plate works from anywhere in the project.

Ruff

Ruff is both the linter (finds problems) and the formatter (fixes layout). One tool, and it is fast enough to run on every save.

uv run ruff format .     # reformat everything
uv run ruff check .      # report problems
uv run ruff check --fix . # fix the ones it can fix automatically

Configure it in pyproject.toml so everyone working on the project gets identical behavior:

[tool.ruff]
line-length = 88

[tool.ruff.lint]
# pycodestyle errors, pyflakes, isort, and common bug patterns.
select = ["E", "F", "I", "B"]

You already turned on format-on-save in the setup guide. With the config above in the repo, the editor and the command line agree, and code review never turns into an argument about whitespace.

Tests

You do not need a comprehensive test suite. You need enough that when you change your normalization code six months from now, you find out immediately if you broke it.

# tests/test_normalize.py
import numpy as np

from assay_analysis.normalize import subtract_blank


def test_subtract_blank_removes_the_blank_mean():
    plate = np.array([[1.0, 2.0], [3.0, 4.0]])
    blanks = np.array([1.0, 1.0])
    result = subtract_blank(plate, blanks)
    assert np.allclose(result, [[0.0, 1.0], [2.0, 3.0]])


def test_subtract_blank_rejects_mismatched_shapes():
    import pytest

    with pytest.raises(ValueError):
        subtract_blank(np.zeros((2, 2)), np.zeros((3,)))

Run them with:

uv run pytest

Two tests like these — one for the expected case, one for the failure you are worried about — already pay for themselves. Write them for the code that does the actual science: the normalization, the curve fitting, the unit conversions. Those are the places where a silent bug becomes a wrong figure in a paper.

Notebooks

Notebooks are excellent for exploring and poor for anything you need to trust twice. The lab’s rule of thumb: explore in a notebook, then move anything you rely on into src/ where it can be tested and imported.

uv add --dev jupyterlab
uv run jupyter lab

Number your notebooks in the order they should be read (01-explore.ipynb, 02-fit-curves.ipynb). Import your own code into them rather than pasting functions in:

from assay_analysis.normalize import subtract_blank

Data

Git is built for text, not for gigabytes of instrument output. Committing raw data makes the repository permanently large — Git keeps everything forever, so deleting the file later does not shrink it.

In the repository: your code, your pyproject.toml and uv.lock, small reference files (plate maps, sample manifests, configuration), and the scripts that produce your figures.

Not in the repository: raw instrument exports, large intermediate arrays, model checkpoints, anything a script can regenerate. Keep these in the lab’s S3 bucket — see the Cloud computing on AWS — and note in your README exactly where they live.

Add this to .gitignore at the start, before you accidentally commit something big:

.venv/
__pycache__/
*.pyc
.ipynb_checkpoints/
data/raw/
data/interim/
*.h5
*.ckpt

Working with Claude Code

Start it from inside the project directory so it can see your code:

cd assay-analysis
claude

Write a CLAUDE.md at the top of the repository. It is loaded automatically at the start of every session, and it is the difference between an assistant that guesses at your conventions and one that follows them:

# CLAUDE.md

Analysis of plate-reader assay data for the polymer screening project.

## Commands
- `uv run pytest` — run tests
- `uv run ruff check --fix .` — lint and autofix
- `uv run fit-plate` — fit dose–response curves for a plate

## Conventions
- Dependencies are managed with uv. Never edit uv.lock by hand.
- Analysis code lives in src/assay_analysis/. Notebooks import from it; they
  do not define functions.
- Raw data is not in this repo — it is in S3 (see README).

Two habits worth forming early. Read the diffs before you accept them — it is your name on the commit and your figure in the paper. And when something matters scientifically, ask for a test alongside the change, so the behavior is pinned down rather than just asserted.

Day-to-day Git

The loop, in the order you will actually use it.

Starting work — always pull first, so you are building on what is current:

git pull

Starting something new — branch, so main stays working:

git switch -c normalize-by-plate

Saving your work — stage, then commit with a message that says why, not what (the diff already shows what):

git add .
git commit -m "Normalize per plate rather than per experiment

Plate-to-plate variation was large enough that the pooled blank
was biasing low-signal wells."

Publishing it — the first push on a new branch needs -u to link it to GitHub; after that git push alone is enough:

git push -u origin normalize-by-plate

Getting it reviewed — open a pull request from the terminal:

gh pr create --fill

Commit often and in small pieces. A commit that changes one thing is one you can understand, review, and undo. A commit called “updates” containing three days of work is none of those.

Appendix: projects that still use pip

You will meet repositories with a requirements.txt and no pyproject.toml — older lab code, a collaborator’s repo, a paper’s supplementary code. They still work:

python -m venv .venv

Windows

.venv\Scripts\Activate.ps1

macOS

source .venv/bin/activate

Linux

source .venv/bin/activate
pip install -r requirements.txt

Your prompt changes to show (.venv) when the environment is active. deactivate leaves it.

uv can also read these directly without you activating anything, which is usually less trouble:

uv venv
uv pip install -r requirements.txt
uv run python your_script.py

If the project is one the lab will keep maintaining, it is worth converting it properly: run uv init in the directory, uv add each dependency from the requirements file, commit the resulting pyproject.toml and uv.lock, and delete requirements.txt.