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

Part 1 of the thread Dev habits for a team of one
- Use a src layout, one
pyproject.tomland a.venvper 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 installonce,pre-commit run --all-filesto check the whole tree, andpre-commit autoupdateevery 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.