Why I Finally Switched to Hatch for Python Builds
Actually, I should clarify — I’ve spent the last decade fighting with Python packaging. I’ve written setup.py files that looked like spaghetti code. I’ve dealt with requirements.txt files that somehow worked on my machine but exploded in CI. I’ve used Poetry, Flit, PDM, and raw setuptools. And honestly? Most of them made me want to quit programming and become a goat farmer.
But around late 2024, I forced myself to try Hatch. Not just for a toy project, but for a messy, dependency-heavy internal tool we use for data processing. It was painful at first—mostly because I had to unlearn years of bad habits—but now, in February 2026, I can’t imagine going back.
For the longest time, Python didn’t really have a standard. We had “recommendations” that everyone ignored. But with PEP 621 (standardizing metadata) and PEP 517 (build backends), things finally stabilized. Hatch—specifically its build backend, Hatchling—feels like the first tool that actually respects these standards without adding a layer of proprietary nonsense on top.
I recently migrated a legacy library from setuptools to Hatch. The setup.py was 180 lines of imperative Python logic just to include some non-code data files. The replacement pyproject.toml? 34 lines. That’s not an exaggeration. It’s just declarative configuration. It works or it doesn’t.
Hatch manages environments for you, but it does it with a “matrix” approach that is incredibly useful for testing. Instead of just one environment, you can define environment types. I was debugging a weird segfault that only happened on Python 3.13 (which is our production target as of last month). I didn’t want to blow up my main dev environment. So I added this:
[tool.hatch.envs.test]
dependencies = [
"pytest",
"pytest-cov",
]
[[tool.hatch.envs.test.matrix]]
python = ["3.11", "3.12", "3.13"]
Then I ran:
hatch run test:pytest
Hatch spun up three isolated environments, ran the tests in all of them, and reported back. I didn’t have to install Docker. I didn’t have to configure Tox (which I’ve always found clunky). It just worked. It felt like I was cheating.
Hatch has a built-in version command that hooks into your code. I configure it to look at a file:
[tool.hatch.version]
path = "src/cruncher/__init__.py"
Now, when I’m ready to release, I just type:
hatch version minor
It updates the file. It’s simple, but it removes one more friction point. And removing friction is the only way you actually ship things instead of sitting on them.
Look, tools are just a means to an end. The goal is to get your code out of your repo and into the hands of users. If your build system is so complex that you dread releasing a new version, your project is already dying. As I’ve argued before, deployment should be boring.
Hatch isn’t perfect. The documentation can be a bit dense sometimes, and I’ve hit a few edge cases with C-extensions that required some fiddling. But compared to the absolute chaos of the last ten years of Python packaging, it’s a breath of fresh air.
It lets me define my project, test it across versions, and build the artifact without having to think about the plumbing. And that means I can stop tweaking config files and actually ship the feature I promised three months ago.
