Skip to content

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)

  1. Module → package → distribution
  2. Minimal module
  3. pyproject.toml — modern standard (PEP 621)
  4. src layout vs flat layout
  5. Editable installs
  6. Package __init__.py patterns
  7. Namespace packages (PEP 420)
  8. Entry points and CLI modules
  9. Relative imports inside packages
  10. __all__ and public API discipline
  11. Testing layout
  12. 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:

"""Core business logic."""

def greet(name: str) -> str:
    return f"Hello, {name}!"

src/mypackage/__init__.py:

"""Public package API."""

from mypackage.core import greet

__all__ = ["greet"]

Consumers:

import mypackage
mypackage.greet("Ada")

from mypackage import greet

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.

python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest

Editable installs#

pip install -e .

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#

from mypackage.core import greet, VERSION

__all__ = ["greet", "VERSION"]
__version__ = "0.1.0"

Subpackage aggregation#

# mypackage/utils/__init__.py
from mypackage.utils.helpers import slugify

__all__ = ["slugify"]

Namespace packages (PEP 420)#

Directories without __init__.py can participate in a shared namespace across distributions:

vendor_a/plants/roses/
vendor_b/plants/tulips/
# both contribute to namespace 'plants'

Rare in application code; common in plugin ecosystems. Regular packages are simpler for most projects.


Entry points and CLI modules#

pyproject.toml:

[project.scripts]
mycli = "mypackage.cli:main"

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:

python -m mypackage.cli

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:

python -m mypackage.utils.helpers

See Modules and Packages.


__all__ and public API discipline#

# core.py
def public(): ...
def _internal(): ...

__all__ = ["public"]

from module import * respects __all__ (discouraged in app code, useful in __init__.py re-exports).


Testing layout#

tests/
├── conftest.py          # shared fixtures
├── test_core.py
└── integration/
    └── test_api.py

pyproject.toml:

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

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
[project]
dependencies = ["mylib @ file:///path/to/mylib"]

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)#

src/mypackage/
├── __init__.py
├── py.typed          # empty file — signals typed package
└── core.py

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#

  1. What is the difference between a module, package, and distribution?
  2. Why does the src layout reduce import path confusion?
  3. What does pip install -e . do?
  4. When are relative imports valid?
  5. What belongs in pyproject.toml vs __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