Creating Modules and Packages#
Moving from scripts to installable packages means organizing code into importable modules, declaring metadata, and choosing a layout that tools (pip, pytest, mypy) understand. This page extends Modules and Packages with pyproject.toml, src layout, entry points, and publishing patterns for Python 3.10+.
How to use this page
Read after Modules and Packages and The Python Environment. For import mechanics and circular imports, review the intermediate page first.
At a glance
| Track | Python Advanced → Modules and Packaging |
| Sections | 17 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Module → package → distribution · Minimal module · pyproject.toml — modern standard (PEP 621) · src layout vs flat layout · Editable installs · Package __init__.py patterns · Namespace packages (PEP 420) · Entry points and CLI modules · … (+9 more)
- Module → package → distribution
- Minimal module
pyproject.toml— modern standard (PEP 621)srclayout vs flat layout- Editable installs
- Package
__init__.pypatterns - Namespace packages (PEP 420)
- Entry points and CLI modules
- Relative imports inside packages
__all__and public API discipline- Testing layout
- Versioning and publishing (overview) … and 5 more sections
Module → package → distribution#
| Term | Meaning |
|---|---|
| Module | Single .py file importable as import name |
| Package | Directory with __init__.py (or namespace package) |
| Distribution | What you publish to PyPI / install with pip |
| Project | Repo + build config + metadata |
myproject/ # project root (git repo)
├── pyproject.toml # build + tool config
├── README.md
├── src/
│ └── mypackage/ # import name: mypackage
│ ├── __init__.py
│ ├── core.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
└── tests/
└── test_core.py
Minimal module#
src/mypackage/core.py:
src/mypackage/__init__.py:
Consumers:
Keep __init__.py thin
Re-export stable public API; avoid heavy import-time work (network, file I/O).
pyproject.toml — modern standard (PEP 621)#
[build-system]
requires = ["hatchling>=1.21"]
build-backend = "hatchling.build"
[project]
name = "mypackage"
version = "0.1.0"
description = "Example package for BrewingIntelligence"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "Ada Lovelace", email = "ada@example.com" }]
dependencies = [
"requests>=2.31",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.3"]
[project.scripts]
mycli = "mypackage.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
| Section | Purpose |
|---|---|
[build-system] |
How to build wheels/sdists |
[project] |
Metadata pip displays |
[project.scripts] |
Console entry points |
[tool.*] |
pytest, mypy, ruff config |
Build backends: hatchling, setuptools, poetry-core, flit.
src layout vs flat layout#
| Layout | Structure | Pros |
|---|---|---|
| src | src/mypackage/ |
Tests import installed code; fewer accidental local imports |
| Flat | mypackage/ at root |
Simpler for tiny projects |
Interview trap — importing local package without install
With flat layout, import mypackage may pick up the repo root instead of installed code. src layout forces pip install -e . before tests see the real package.
Editable installs#
Changes in src/mypackage/ reflect immediately without reinstall — essential for development.
Package __init__.py patterns#
Empty __init__.py#
Marks directory as regular package; submodules import as mypackage.utils.helpers.
Re-export API#
Subpackage aggregation#
Namespace packages (PEP 420)#
Directories without __init__.py can participate in a shared namespace across distributions:
Rare in application code; common in plugin ecosystems. Regular packages are simpler for most projects.
Entry points and CLI modules#
pyproject.toml:
src/mypackage/cli.py:
import sys
def main(argv: list[str] | None = None) -> int:
argv = argv or sys.argv[1:]
print(f"Hello CLI: {argv}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
After install, mycli is on PATH. Also runnable as:
Details: Building Command Line Interfaces.
Relative imports inside packages#
From mypackage/utils/helpers.py:
from mypackage.core import greet # absolute (preferred)
from ..core import greet # relative — one level up
Relative imports require package context — fail when running file as script. Use:
See Modules and Packages.
__all__ and public API discipline#
from module import * respects __all__ (discouraged in app code, useful in __init__.py re-exports).
Testing layout#
pyproject.toml:
Or rely on editable install so import mypackage resolves correctly.
Versioning and publishing (overview)#
# build artifacts
pip install build
python -m build
# dist/mypackage-0.1.0-py3-none-any.whl
# dist/mypackage-0.1.0.tar.gz
# publish (with credentials)
pip install twine
twine upload dist/*
Use semantic versioning (MAJOR.MINOR.PATCH). Pin requires-python honestly — 3.10+ features break on 3.9.
Private packages and monorepos#
| Pattern | Use |
|---|---|
pip install -e ./libs/foo |
Local path dependency in pyproject.toml |
uv / poetry workspaces |
Multiple packages one repo |
| Private index | pip install --index-url |
Common packaging mistakes#
| Mistake | Symptom | Fix |
|---|---|---|
No pyproject.toml |
Manual setup.py drift |
Migrate to PEP 621 |
| Tests import uninstalled src | False positives | Editable install or src layout |
__init__.py side effects |
Slow/surprising imports | Lazy imports |
| Name clashes with stdlib | Shadow imports | Rename (json.py → never) |
Missing MANIFEST.in / package data |
Resources missing in wheel | tool.setuptools.package-data or hatch files |
py.typed marker (PEP 561)#
Include in wheel so consumers' type checkers use your annotations.
Security practices#
Never publish secrets in packages
API keys in __init__.py, .env in sdist, or credentials in pyproject.toml end up on PyPI mirrors. Use environment variables — Managing Secrets.
| Check | Action |
|---|---|
.gitignore |
Exclude .env, dist/, .venv/ |
twine check |
Validate metadata before upload |
| Dependency pinning | Audit with pip-audit |
Minimal end-to-end workflow#
# 1. Create venv
python3.12 -m venv .venv && source .venv/bin/activate
# 2. Scaffold (manual or cookiecutter)
mkdir -p src/mypackage tests
# 3. Write pyproject.toml (see above)
# 4. Editable install + dev deps
pip install -e ".[dev]"
# 5. Develop, test, type-check
pytest
mypy src
# 6. Build & release
python -m build && twine upload dist/*
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
python utils.py with relative imports |
ImportError |
python -m package.module |
| Flat layout test pollution | Tests pass locally, fail when installed | src layout + editable install |
__init__.py does I/O |
Import side effects | Lazy import inside functions |
| Namespace vs regular package | Subtle import paths | Default to regular packages |
Forgetting requires-python |
Install on wrong Python version | Declare >=3.10 explicitly |
Mental model checklist#
- What is the difference between a module, package, and distribution?
- Why does the
srclayout reduce import path confusion? - What does
pip install -e .do? - When are relative imports valid?
- What belongs in
pyproject.tomlvs__init__.py?
What's next#
| Topic | Page |
|---|---|
| Import fundamentals | Modules and Packages |
| Environment / venv | The Python Environment |
| CLI apps | Building Command Line Interfaces |
| Unit testing | Unit Testing |
| Type hints | Type Hints and Annotations |
| Secrets | Managing Secrets |