Skip to content

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

FixDevs ·

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"

Still Not Working?

Hatch vs Poetry vs uv

  • Hatch — PyPA-official, supports matrix envs, decent default build backend. Best for libraries published to PyPI.
  • Poetry — Mature, opinionated, single-file lock format. Best for application development with strict reproducibility. See Poetry dependency conflict.
  • uv — Rust-based, extremely fast, growing rapidly. Best for performance-sensitive workflows. See uv not working.

For new library projects, Hatch is a safe default. For applications, uv has the strongest momentum. Poetry remains common in older codebases.

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. For pytest fixture patterns that pair with Hatch environments, see pytest fixture not found.

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, see pre-commit not working:

# .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]
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