Codeskill

Learn to code, step by step

Packaging your module

Turning a folder of .py files into a package you can import reliably. The goal is structure that survives moving from ‘script on my laptop’ to ‘project I pick up again in six months’.

A sensible folder layout

Start small. A single package with tests beside it is enough for most learning projects and many production utilities.

mytool/
├── pyproject.toml
├── README.md
├── src/
│   └── mytool/
│       ├── __init__.py
│       ├── cli.py
│       └── helpers.py
└── tests/
    └── test_helpers.py

The src/ layout keeps import mistakes honest: you test the installed package, not accidental imports from the project root.

What goes in __init__.py

__init__.py marks a directory as a package. It can be empty, or it can expose a tidy public API.

# src/mytool/__init__.py
from .helpers import normalise_email

__all__ = ["normalise_email"]
__version__ = "0.1.0"

__all__ documents what you consider public. It also limits from mytool import *, which nobody should use in serious code anyway.

pyproject.toml basics

Modern Python projects declare metadata and build settings in pyproject.toml. You do not need every field on day one.

[project]
name = "mytool"
version = "0.1.0"
description = "Small utilities for tidying contact data"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []

[project.scripts]
mytool = "mytool.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

The [project.scripts] entry creates a command-line entry point when the package is installed. We use that pattern again in the CLI tutorials.

Editable installs while you develop

Install your package in editable mode so changes to source files are picked up immediately without reinstalling.

python -m venv .venv
source .venv/bin/activate  # Windows: .venvScriptsactivate
pip install -e ".[dev]" 

The -e flag means editable. Quote the path so your shell does not misread the brackets.

Modules versus scripts

A script runs top to bottom when executed. A module defines behaviour other code imports. Keep reusable logic in modules; keep thin wrappers in scripts or CLI entry points.

# src/mytool/helpers.py
def normalise_email(value: str) -> str:
    return value.strip().lower()

# src/mytool/cli.py
from mytool.helpers import normalise_email

def main() -> None:
    print(normalise_email("  Alice@Example.com  "))

if __name__ == "__main__":
    main()

Naming and imports

Package names should be short, lowercase, and unique enough for PyPI if you ever publish. Import your own code with absolute imports from the package root.

# good
from mytool.helpers import normalise_email

# avoid in packaged code
from ..helpers import normalise_email

Packaging is mostly discipline: one obvious place for each concern, a version number you bump, and imports that work the same on your machine and a colleague’s. Next: virtual environments and pinning dependencies so installs reproduce reliably.

PreviousIdiomatic Python