Src-Layout Structure in Hatchling
Overview
The src-layout is a project structure where Python packages are placed in a src/ directory rather than at the project root. When helping users organize their Python projects, reference Hatchling's excellent support for this layout with automatic detection and configuration.
Why Use Src-Layout?
Benefits
- Import isolation: Prevents accidentally importing from the development directory
- Cleaner separation: Distinguishes source code from project files
- Testing integrity: Ensures tests run against installed package, not local files
- Tool compatibility: Better support for type checkers and linters
Structure Comparison
Flat layout:
myproject/
├── mypackage/
│ └── __init__.py
├── tests/
└── pyproject.toml
Src-layout:
myproject/
├── src/
│ └── mypackage/
│ └── __init__.py
├── tests/
└── pyproject.toml
Basic Configuration
Automatic Detection
Hatchling automatically detects src-layout:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mypackage"
version = "1.0.0"
# No additional configuration needed!
# Hatchling detects src/mypackage/ automatically
Explicit Configuration
For explicit control:
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
Package Discovery
Single Package
src/
└── mypackage/
├── __init__.py
├── module1.py
└── module2.py
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
Multiple Packages
src/
├── package_a/
│ └── __init__.py
└── package_b/
└── __init__.py
[tool.hatch.build.targets.wheel]
packages = ["src/package_a", "src/package_b"]
Auto-discovery with Sources
[tool.hatch.build.targets.wheel]
# Remove 'src/' prefix from installed packages
sources = ["src"]
Single Module Layout
Auto-detection
Hatchling auto-detects single module layouts:
src/
└── mymodule.py # Single module, no package directory
[project]
name = "mymodule"
# Hatchling automatically detects and includes mymodule.py
Explicit Single Module
[tool.hatch.build.targets.wheel]
packages = ["src"]
include = ["src/mymodule.py"]
Complex Src-Layouts
Nested Packages
src/
└── company/
├── __init__.py
└── products/
├── __init__.py
├── webapp/
│ └── __init__.py
└── api/
└── __init__.py
[tool.hatch.build.targets.wheel]
packages = ["src/company"]
Mixed Layout
project/
├── src/ # Source code
│ └── myapp/
│ └── __init__.py
├── scripts/ # Executable scripts
│ └── runner.py
└── data/ # Data files
└── config.json
[tool.hatch.build.targets.wheel]
packages = ["src/myapp"]
artifacts = ["data/*.json"]
[tool.hatch.build.targets.wheel.force-include]
"scripts/runner.py" = "myapp/scripts/runner.py"
Development Workflow
Editable Installation
# Install in editable mode
pip install -e .
# Or using Hatch
hatch shell
Import Path Resolution
With src-layout, imports work correctly:
# In tests/test_module.py
from mypackage import something # Imports from installed package
# NOT from ../mypackage
Development Mode Configuration
[tool.hatch.build]
dev-mode-dirs = ["src"] # Explicitly set development directories
Testing with Src-Layout
Test Structure
project/
├── src/
│ └── mypackage/
│ └── __init__.py
├── tests/
│ ├── __init__.py
│ └── test_mypackage.py
└── pyproject.toml
Test Configuration
[tool.hatch.envs.default]
dependencies = [
"pytest",
"pytest-cov"
]
[tool.hatch.envs.default.scripts]
test = "pytest tests/ --cov=mypackage"
Import in Tests
# tests/test_mypackage.py
import pytest
from mypackage import MyClass # Clean import
def test_something():
assert MyClass().method() == expected
Type Checking Support
MyPy Configuration
[tool.mypy]
packages = ["mypackage"]
mypy_path = "src"
Pyright Configuration
{
"include": ["src"],
"typeCheckingMode": "strict"
}
Legacy Support
Supporting Legacy Tools
For tools that don't understand src-layout:
[tool.hatch.build.targets.sdist]
support-legacy = true # Creates PKG-INFO at root level
Migration to Src-Layout
From Flat Layout
Before:
myproject/
├── mypackage/
│ └── __init__.py
└── pyproject.toml
After:
myproject/
├── src/
│ └── mypackage/
│ └── __init__.py
└── pyproject.toml
Steps:
- Create
src/directory - Move package directory to
src/ - Update imports in tests if needed
- Update pyproject.toml if using explicit configuration
Configuration Update
# Old (flat layout)
[tool.hatch.build.targets.wheel]
packages = ["mypackage"]
# New (src-layout)
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
# Or just remove it for auto-detection
Build Artifacts
What Gets Built
With src-layout configuration:
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
The wheel contains:
mypackage/
├── __init__.py
├── module1.py
└── module2.py
Note: The src/ directory is not included in the wheel.
Advanced Configurations
Custom Source Mappings
[tool.hatch.build.targets.wheel.sources]
"src" = "" # Remove src prefix
"src/old_name" = "new_name" # Rename during build
Including Additional Files
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
include = [
"LICENSE",
"README.md"
]
artifacts = [
"src/mypackage/data/*.json"
]
Namespace Packages with Src-Layout
src/
└── namespace/ # No __init__.py (PEP 420)
└── subpackage/
└── __init__.py
[tool.hatch.build.targets.wheel]
packages = ["src/namespace"]
Best Practices
When recommending src-layout to users, reference these best practices:
- Always use src-layout for libraries: Guide users to prevent testing accidents
- Keep tests outside src/: Remind users that tests shouldn't be distributed
- Use editable installs for development: Recommend
pip install -e .for development workflows - Configure tools properly: Help users ensure linters/type-checkers know about src/
- Document the structure: Encourage users to document the layout for contributors
Common Issues and Solutions
Issue: Import Errors During Development
Problem: Users can't import package during development
Solution: Guide users to install in editable mode:
# Install in editable mode
pip install -e .
Issue: Tests Import Wrong Package
Problem: Users' tests import local files instead of installed package
Solution: Help users use src-layout and run tests from project root:
cd myproject
pytest tests/ # Not from within tests/
Issue: Tools Don't Find Package
Problem: Users' linters or type checkers can't find the package
Solution: Guide users to configure their tools:
# For tools that need explicit paths
[tool.hatch.build]
dev-mode-dirs = ["src"]
# For pytest
[tool.pytest.ini_options]
pythonpath = ["src"]
Complete Example
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "awesome-package"
version = "1.0.0"
description = "An awesome package using src-layout"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [{name = "Your Name", email = "you@example.com"}]
dependencies = [
"requests>=2.28",
"click>=8.0"
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"mypy>=1.0",
"ruff>=0.1"
]
[tool.hatch.build.targets.wheel]
packages = ["src/awesome_package"]
[tool.hatch.build.targets.sdist]
include = [
"src/",
"tests/",
"README.md",
"LICENSE"
]
[tool.ruff]
src = ["src"]
[tool.mypy]
mypy_path = "src"
packages = ["awesome_package"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
Directory structure:
awesome-package/
├── src/
│ └── awesome_package/
│ ├── __init__.py
│ ├── core.py
│ └── cli.py
├── tests/
│ ├── test_core.py
│ └── test_cli.py
├── README.md
├── LICENSE
└── pyproject.toml