Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Engineering

Engineering

Scaffolding a Python Project So Future-You Doesn't Hate It

Metal scaffolding and a yellow ladder against a blue sky.

Part 1 of the thread Dev habits for a team of one

THE SHORT VERSION4 points
  • Use a src layout, one pyproject.toml and a .venv per project. Boring on purpose.
  • My notes wire up Black, flake8 and isort separately. Ruff now does all three jobs from one tool.
  • Add mypy, pytest and a pre-commit hook so the checks run without you remembering to.
  • If Claude generates the scaffold, ask for working code, then read every file it made.

A lot of Python projects start the same way: one main.py, "I'll organize it later," and six months on a folder nobody wants to touch. The fix is a little dull setup at the start. My vault has a whole workflow for it, including a prompt that has Claude generate the structure for me.

Checked against the docs in September 2026.

The layout

This is the shape I aim for. It's the src layout:

my-tool/
  pyproject.toml
  README.md
  .gitignore
  .pre-commit-config.yaml
  src/
    my_tool/
      __init__.py
      cli.py
  tests/
    test_cli.py

Why src/? The Python Packaging User Guide lays out both options without picking one, but it's clear about the src layout's main benefit: it stops you from accidentally importing the copy in your working folder instead of the installed one. That's a whole category of "works on my machine" that just goes away.

Then a virtual environment per project, and an editable install so your tests import the package the way a user would:

python3 -m venv .venv
source .venv/bin/activate
pip install -e .

On Windows the middle line is .venv\Scripts\activate. My notes called the folder venv, and .venv is the name the packaging guide uses now. Either works. The dot just keeps it out of the way.

One config file

Everything goes in pyproject.toml. My notes spread the tool settings across pyproject.toml, a separate .flake8 file and a couple of helper scripts. Here's the single-file version:

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "my-tool"
version = "0.1.0"
requires-python = ">=3.11"

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I", "B"]

[tool.mypy]
strict = true

[tool.pytest.ini_options]
testpaths = ["tests"]

Where my notes fell behind

My workflow sets up six tools: Black for formatting, flake8 for linting, isort for import order, mypy for types, pytest for tests and bandit for security. It also spends a paragraph on keeping Black and flake8 from arguing, since they disagree about a couple of whitespace rules.

Ruff makes most of that moot. Its docs say it replaces flake8 and isort, and its formatter is designed as a drop-in replacement for Black. One tool, one config section, and no argument to referee:

ruff check --select I --fix
ruff format
ruff check

The first line sorts imports, the second formats, and the third lints. The rest of the list still earns its spot:

Tool Job Note
Ruff Lint, format, sort imports Replaces Black, flake8 and isort
mypy Type checking strict = true on new code, loosen it for old code
pytest Tests Add pytest-cov and run pytest --cov to see what isn't tested
bandit Security linting Catches things like hardcoded passwords and shell injection
pre-commit Runs the above on every commit So you don't have to remember

If you'd rather stay with Black and flake8, that's fine too. Black's current docs suggest extend-ignore = E203,E701 for flake8, and W503 is off by default now, so my old ignore list is out of date either way.

Tip

Turn on pre-commit hooks on day one, while the project is small and passes everything. pre-commit install once, pre-commit run --all-files to check the whole tree, and pre-commit autoupdate every so often to bump the hook versions. Adding it to a year-old project means fixing a year of warnings first.

Letting Claude write the scaffold

The prompt in my notes is short: project type, project name, then a list of what the structure has to include. The line that matters most is the last one:

Generate working code, not placeholders. Include example tests and proper package structure.

Without it you get a lovely tree of files full of pass and # TODO. With it you get a CLI that runs, a test that passes and a config that actually loads. Then read every file anyway. A generated scaffold is still code you're signing up to maintain, and it's easier to question a choice on day one than on day ninety.

If you want Claude to keep following the setup after day one, put the commands in the project's CLAUDE.md. And for the older projects that never got any of this, the next post in the thread is about keeping an old project alive without breaking it.

Not everything needs all of it, though. The Conversation Project Kit is plain standard-library Python with no dependencies at all, and that was the right call for a tool meant to still run in three years.