Fix: Hatch Not Working — Environment Errors, Build Backend, and pyproject.toml Issues
Part of: Python Errors
Quick Answer
How to fix Hatch errors — hatch env create fails, scripts not found, build backend hatchling missing, version not detected, plugin install errors, and publishing to PyPI.
The Error
You install Hatch and try to create an environment — nothing happens:
$ hatch env create
# Silent. No output. Did it work?Or you define scripts in pyproject.toml and they’re not found:
$ hatch run test
Command not found: testOr the build fails because Hatchling can’t determine the version:
ValueError: Unable to determine version. Check that 'src/mypkg/__init__.py' contains __version__Or you migrate from Poetry/setuptools and hatch build succeeds but the wheel is empty:
$ unzip -l dist/mypkg-1.0.0-py3-none-any.whl
# wheel only contains metadata — no Python filesOr hatch publish to PyPI fails with auth errors:
HTTPError: 403 ForbiddenHatch is the official PyPA project manager — it manages virtual environments, runs scripts, builds packages, and publishes to PyPI. Unlike Poetry (third-party) or uv (Rust-based newcomer), Hatch is maintained by the PyPA team that designs Python packaging standards. The two halves — hatch (CLI/env manager) and hatchling (build backend) — confuse newcomers because they’re usually used together but are separate tools. This guide covers each common failure.
Why This Happens
Hatch creates isolated virtual environments for each named env in pyproject.toml. The first hatch env create or hatch run lazily creates the env and installs dependencies — the silent output is normal completion. Scripts defined under [tool.hatch.envs.default.scripts] only work via hatch run <script>, not as raw shell commands.
Hatchling (the build backend) needs an explicit version source — it doesn’t auto-detect __version__ unless you configure [tool.hatch.version]. Empty wheels happen when the package discovery config points at the wrong directory.
Fix 1: Installing Hatch
# Standalone install (recommended — isolated, includes interpreter)
brew install hatch # macOS
pipx install hatch # Cross-platform
uv tool install hatch # Via uv
pip install --user hatch # Via pip (may conflict with project envs)Verify install:
hatch --version
hatch python list # Shows available Pythons Hatch can useHatch is BOTH a project manager AND a venv manager — it can install Python interpreters too:
hatch python install 3.12 # Download and install Python 3.12
hatch python install all # Install all supported versions
hatch python show # Show installed PythonsUseful when system Python isn’t recent enough. Hatch downloads from python.org / python-build-standalone.
Common Mistake: Installing Hatch via pip install hatch into the same venv as the project being managed. This creates a circular dependency — when Hatch tries to recreate the env, it’d uninstall itself. Use pipx, uv tool install, or brew to install Hatch globally and isolated.
Fix 2: Minimal pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mypackage"
dynamic = ["version"]
description = "My awesome package"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{ name = "Your Name", email = "[email protected]" },
]
dependencies = [
"requests>=2.31",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
docs = ["sphinx", "furo"]
[project.scripts]
mycli = "mypackage.cli:main" # Console script entry point
[tool.hatch.version]
path = "src/mypackage/__init__.py"
[tool.hatch.envs.default]
dependencies = ["pytest", "ruff", "mypy"]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
lint = "ruff check ."
format = "ruff format ."
type-check = "mypy src/"
all = ["lint", "type-check", "test"]Project layout Hatchling expects by default:
my-project/
├── pyproject.toml
├── README.md
├── src/
│ └── mypackage/
│ ├── __init__.py # Contains __version__ = "1.0.0"
│ └── cli.py
└── tests/
└── test_basic.pyBuild and check:
hatch build # Creates dist/*.whl and dist/*.tar.gz
hatch build --clean # Clean dist/ first
unzip -l dist/*.whl # Verify wheel contents include your packageFix 3: Environments and Scripts
hatch env create # Create the default env
hatch env show # List all envs
hatch shell # Activate the default env in a subshell
hatch env remove default # Delete an envRun scripts defined in pyproject.toml:
hatch run test # Runs the "test" script in default env
hatch run lint # Runs lint
hatch run all # Runs lint, type-check, test in sequence
hatch run test -- -v # Pass extra args to pytest via --Multiple environments for testing matrix:
[tool.hatch.envs.default]
dependencies = ["pytest"]
[[tool.hatch.envs.test.matrix]]
python = ["3.10", "3.11", "3.12", "3.13"]
deps = ["pydantic-1", "pydantic-2"]
[tool.hatch.envs.test.overrides]
matrix.deps.dependencies = [
{ value = "pydantic<2", if = ["pydantic-1"] },
{ value = "pydantic>=2", if = ["pydantic-2"] },
]hatch env show # Lists test.py3.10-pydantic-1, test.py3.10-pydantic-2, ...
hatch run test:pytest # Runs pytest in EVERY matrix comboEquivalent to Tox/Nox matrix without the separate config file.
Common Mistake: Running pytest directly instead of hatch run test. Without hatch run, you’re using whatever pytest is on your system PATH — not necessarily the one in the project env. The test passes/fails locally but fails differently in CI because the dependency versions differ. Always use hatch run to ensure the right env.
Fix 4: Version Management
[tool.hatch.version]
path = "src/mypackage/__init__.py"# src/mypackage/__init__.py
__version__ = "1.2.3"Hatch reads __version__ and uses it for builds — no separate version file needed.
Bump versions:
hatch version # Show current version
hatch version patch # 1.2.3 → 1.2.4
hatch version minor # 1.2.3 → 1.3.0
hatch version major # 1.2.3 → 2.0.0
hatch version 2.0.0a1 # Set explicit versionPro Tip: Use hatch version to bump versions instead of editing __init__.py manually. It updates the file, normalizes the version string (PEP 440), and validates the format. Combined with conventional commits and CI tagging, this gives you a clean release workflow without separate version-management tools.
Version from git tags (alternative):
[tool.hatch.version]
source = "vcs" # Read from git tagpip install hatch-vcs # Required plugin for VCS-based versionsNow hatch build reads version from git describe — no __version__ needed in code.
Fix 5: Build Targets — wheel and sdist
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
[tool.hatch.build.targets.sdist]
include = [
"src/",
"tests/",
"README.md",
"LICENSE",
]
exclude = [
"**/*.pyc",
"**/__pycache__",
]Common Mistake: Empty wheels happen when Hatchling can’t find your package. Default discovery looks for src/<package_name>/ matching the project name. If your layout differs, set packages = [...] explicitly:
# Flat layout (no src/)
[tool.hatch.build.targets.wheel]
packages = ["mypackage"]
# Different name than project
# Project name: "my-package"; actual import name: "myPackage"
[tool.hatch.build.targets.wheel]
packages = ["src/myPackage"]Verify the wheel includes your code:
hatch build
unzip -l dist/mypackage-*.whl
# Should show: mypackage/__init__.py, mypackage/cli.py, etc.If only *.dist-info/ files appear, your packages config is wrong.
Include non-Python files:
[tool.hatch.build.targets.wheel.force-include]
"src/mypackage/templates" = "mypackage/templates"
"src/mypackage/data/config.json" = "mypackage/data/config.json"Fix 6: Type Hints and Editable Installs
hatch env create # Creates env with package installed in editable mode by default
hatch shell # Activate; your local code changes are reflectedHatch installs your project in editable mode automatically — changes to source files take effect without reinstall.
For type checking with stubs, include a py.typed marker:
[tool.hatch.build.targets.wheel.shared-data]
"src/mypackage/py.typed" = "mypackage/py.typed"touch src/mypackage/py.typedThis tells mypy/pyright/Pylance that your package ships with type hints. Without it, type checkers treat your code as untyped.
Stub-only packages (when distributing type stubs separately):
[project]
name = "types-mypackage"
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage-stubs"]Fix 7: Publishing to PyPI
# Build
hatch build
# Publish to PyPI (requires API token)
hatch publish
# Publish to TestPyPI first (recommended for new packages)
hatch publish -r testConfigure credentials via env vars:
# PyPI
export HATCH_INDEX_AUTH=pypi-<your-api-token>
# Or per-repo
export HATCH_INDEX_REPO=https://upload.pypi.org/legacy/Or via Hatch config:
# pyproject.toml — public; don't put real tokens here
[tool.hatch.publish.indexes.main]
url = "https://upload.pypi.org/legacy/"# ~/.config/hatch/config.toml — keep tokens here
[publish.index.repos.main]
url = "https://upload.pypi.org/legacy/"
user = "__token__"
auth = "pypi-AgEIcHlwaS5vcmc..."Trusted publishers (GitHub Actions, no token needed):
# .github/workflows/publish.yml
name: Publish
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for trusted publishing
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install hatch
- run: hatch build
- uses: pypa/gh-action-pypi-publish@release/v1Configure trusted publisher in PyPI project settings — no API token in GitHub Secrets needed.
Pro Tip: Always publish to TestPyPI first when a package is new or has structural changes. Install from TestPyPI, verify the install works, then publish to real PyPI. Mistakes on real PyPI (wrong file contents, broken metadata) can’t be fixed by re-uploading — versions are immutable once published.
Fix 8: Plugins and Custom Build Hooks
Hatch supports plugins via [tool.hatch.build.hooks.custom]:
[tool.hatch.build.hooks.custom]
path = "hatch_hooks.py"# hatch_hooks.py
from hatchling.plugin import hookimpl
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
def initialize(self, version, build_data):
# Runs before the build
# e.g., generate code, compile assets
print(f"Building {self.metadata.name} {version}")Pre-built plugins:
| Plugin | Purpose |
|---|---|
hatch-vcs | Version from git tags |
hatch-fancy-pypi-readme | Generate PyPI README from multiple sources |
hatch-requirements-txt | Read deps from requirements.txt |
hatchling-build-cuda | Build CUDA extensions |
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"Platform Differences: Hatch vs Poetry vs PDM vs uv vs Rye vs setuptools
Each tool answers the same question — manage envs, build, publish — but the failure modes diverge once you pick one. Knowing where Hatch sits explains many of its quirks.
Hatch (PyPA official) — Two-layer design: hatch CLI manages envs and matrix runs; hatchling is the build backend. Strength: matrix environments and Python distribution management. Weakness: no native lock file (as of 1.13), no dependency resolver beyond pip’s. The [tool.hatch.envs.*] config is verbose compared to Poetry’s [tool.poetry.dev-dependencies].
Poetry — Opinionated all-in-one. Uses its own tool.poetry section instead of standard [project] (Poetry 1.5+ supports [project] but most existing repos still use the legacy section). Lock file poetry.lock is the reproducibility anchor. Weakness: slow resolver on large dep graphs, build backend poetry-core lacks some Hatchling features. See Poetry dependency conflict for resolver issues.
PDM — PEP 621-native from day one (uses [project] directly). Supports PEP 582 __pypackages__ (no venv needed) plus standard venv mode. Lock file is pdm.lock. Strength: closest spec compliance. Weakness: smaller ecosystem of plugins than Poetry. See PDM not working for env activation issues.
uv (Astral) — Rust-based, ~10-100x faster install than pip. Drop-in pip replacement (uv pip install), but also has its own project workflow (uv add, uv sync, uv lock). As of 2026, the most-recommended tool for new projects. Build backend integration uses standard [build-system] — works with Hatchling, setuptools, or maturin. See uv not working for migration tips.
Rye (now merged into uv) — Astral acquired Rye and is folding its features into uv. Use uv for new work; Rye projects still function but won’t get new features.
setuptools (legacy) — The original Python build backend. Still works, supported indefinitely. Modern setuptools reads [project] from pyproject.toml. Use it when you need C extensions and complex build logic that Hatchling doesn’t support; otherwise prefer Hatchling for cleaner config.
hatch-vcs vs setuptools-scm — Both read version from git tags. hatch-vcs is a thin wrapper around setuptools_scm under the hood, so configuration behaves identically. If you migrate from setuptools, switch to hatch-vcs without changing the tag format.
Build backend swap is trivial — [build-system] is the only place that changes. You can use Hatchling as the build backend even if you’re using Poetry or uv as the env manager, and vice versa. The two layers are independent.
Decision matrix:
| Need | Pick |
|---|---|
| Fastest installs, modern workflow | uv |
| Library published to PyPI | Hatch (or uv with hatchling backend) |
| Strict reproducibility for apps | Poetry or uv |
| Spec compliance and PEP 582 | PDM |
| C extensions, complex builds | setuptools or maturin |
Still Not Working?
Hatch’s Place in the Tooling Landscape
For new library projects, Hatch is a safe default — PyPA backing means it tracks packaging spec changes immediately. For applications, uv has the strongest momentum due to install speed and the unified workflow. Poetry remains common in existing codebases and is still actively developed.
Migration from setup.py / setup.cfg
Hatch / Hatchling read entirely from pyproject.toml. Migrate by:
- Move
setup.pymetadata to[project]inpyproject.toml - Move
install_requirestodependencies - Move
extras_requireto[project.optional-dependencies] - Move
entry_pointsto[project.scripts]and[project.entry-points] - Delete
setup.pyandsetup.cfg
Most modern build backends (hatchling, setuptools, flit, pdm-backend) read identical [project] metadata — switching backends is a one-line change in [build-system].
CI Setup with Matrix Testing
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install hatch
- run: hatch run testFor tox-based testing workflows that Hatch can replace, see Tox not working — Hatch’s matrix envs cover most tox use cases without a separate config file.
Environment Storage Location
Hatch stores envs in a shared location by default (not in the project directory). View where:
hatch env find # Path to current env
hatch env find default # Path to a specific named envDefault location:
- macOS/Linux:
~/Library/Application Support/hatch/env/virtual/... - Linux (XDG):
~/.local/share/hatch/env/virtual/... - Windows:
%APPDATA%/hatch/env/virtual/...
Use project-local envs by setting:
[tool.hatch.envs.default]
type = "virtual"
path = ".venv"Now Hatch creates .venv/ in the project root — friendly for VS Code’s auto-detection, easier to delete with rm -rf .venv.
Custom Build Backends
Hatchling is the default but Hatch can drive any PEP 517 backend:
[build-system]
requires = ["setuptools>=68", "setuptools-scm"]
build-backend = "setuptools.build_meta"Hatch still manages envs and scripts even with a non-Hatchling backend. Useful when migrating gradually from setuptools.
Lock Files
Hatch doesn’t have built-in lock file support (as of v1.13). For deterministic CI, either:
- Pin all dependencies in
pyproject.toml(requests==2.31.0, not>=2.31) - Use
pip-toolsto generate arequirements.lockfrompyproject.toml - Use
uv pip compile pyproject.toml -o requirements.lock
If lock files are essential, Poetry or uv provide first-class support.
Combining with Pre-commit
For pre-commit integration that runs Hatch scripts on commit:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: hatch-test
name: Run tests
entry: hatch run test
language: system
pass_filenames: false
stages: [pre-push]Windows Path Length and Long Dependencies
On Windows, Hatch’s shared env location under %APPDATA%/hatch/env/virtual/... plus deeply nested package paths (e.g., tensorflow, nvidia-cudnn-cu12) easily blow past Windows’ 260-character path limit. Symptoms include random FileNotFoundError during install and corrupted DLL extraction. Either enable long paths via gpedit.msc → Computer Configuration → Administrative Templates → System → Filesystem → Enable Win32 long paths, or move envs into the project with [tool.hatch.envs.default] path = ".venv".
Conflicting Build Backends in the Same Repo
If your repo contains both setup.py and pyproject.toml with [build-system] build-backend = "hatchling.build", pip’s PEP 517 isolation honors pyproject.toml but legacy tools (some IDEs, older pip versions, Read the Docs default config) still pick up setup.py. Delete setup.py entirely after migrating; keeping it “just in case” causes intermittent reproducibility bugs.
Editable Install Surprises with src/ Layout
Hatch installs editable by default, but if your pyproject.toml lacks [tool.hatch.build.targets.wheel] packages = ["src/mypackage"], the editable install silently uses the wrong import path. import mypackage returns ModuleNotFoundError even though hatch env create succeeded. Always confirm packages = [...] is set when you use a src/ layout — the default discovery only works when the package name matches the project name exactly.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Pipenv Not Working — Lock File Generation, Shell Activation, and Dependency Resolution
How to fix Pipenv errors — pipenv lock takes forever, Pipfile.lock not generated, shell activation broken, no virtualenv created, dependency conflict, and migration to uv or Poetry.
Fix: PDM Not Working — Lock File Errors, PEP 582 Confusion, and Script Issues
How to fix PDM errors — pdm install fails resolving dependencies, lock file outdated warning, __pypackages__ vs venv confusion, pdm run script not found, build backend missing, and dependency groups setup.
Fix: uv Not Working — Command Not Found, Python Version Error, and Lock File Conflicts
How to fix uv errors — uv command not found after install, no Python interpreter found, uv run vs activate confusion, uv.lock merge conflicts, uv pip vs uv add, migrating from pip and Poetry, and workspace resolution failures.
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.