Skip to content

Fix: Hatch Not Working — Environment Errors, Build Backend, and pyproject.toml Issues

FixDevs · (Updated: )

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: test

Or 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 files

Or hatch publish to PyPI fails with auth errors:

HTTPError: 403 Forbidden

Hatch 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 use

Hatch 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 Pythons

Useful 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.py

Build 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 package

Fix 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 env

Run 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 combo

Equivalent 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 version

Pro 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 tag
pip install hatch-vcs   # Required plugin for VCS-based versions

Now 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 reflected

Hatch 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.typed

This 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 test

Configure 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/v1

Configure 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:

PluginPurpose
hatch-vcsVersion from git tags
hatch-fancy-pypi-readmeGenerate PyPI README from multiple sources
hatch-requirements-txtRead deps from requirements.txt
hatchling-build-cudaBuild 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:

NeedPick
Fastest installs, modern workflowuv
Library published to PyPIHatch (or uv with hatchling backend)
Strict reproducibility for appsPoetry or uv
Spec compliance and PEP 582PDM
C extensions, complex buildssetuptools 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:

  1. Move setup.py metadata to [project] in pyproject.toml
  2. Move install_requires to dependencies
  3. Move extras_require to [project.optional-dependencies]
  4. Move entry_points to [project.scripts] and [project.entry-points]
  5. Delete setup.py and setup.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 test

For 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 env

Default 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-tools to generate a requirements.lock from pyproject.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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles