---
# System prepended metadata

title: Python Packaging 101 tutorial

---

# Python Packaging 101 tutorial

Welcome! This is a hands-on introduction to packaging Python projects using modern tooling.

## Learning outcomes
By the end of this tutorial, you will:
- Understand the structure of a modern Python package.
- Build source and wheel distributions with `python -m build`.
- Upload to TestPyPI using twine and an API token.
- Install and verify your package from TestPyPI.

## Setup and requirements
- Computer connected to the Internet
- Access to your personal GitHub account 
- Python 3.9+, `pip`, `venv` or `virtualenv` installed
- Access to your personal [TestPyPI](https://test.pypi.org) account (optional, we will have time to do it in class)

## Prerequisites
To get the most out of this tutorial, you should be comfortable with:
- Writing basic Python code 
- Using virtual environments 
- Using terminal


# Overview

## Why package?
- Make your code reusable and shareable.
- Control versions and dependencies.
- Enable reproducible installs.


# 1. Structure of an Installable Module


[acpackage](https://github.com/albuscode/acpackage) is a demo package we will use for this tutorial.
Let's look at the file directory:


```
acpackage/
  src/
    acpackage/
      __init__.py
      example.py
  docs/ 
  tests/ 
  pyproject.toml
  README.md
  LICENSE
  .gitignore
```
A file directory is a hierarchical structure, like a physical filing cabinet, that organizes files into folders.
`.gitignore`, `docs/`,  `tests/` are optional, but highly recommended


**src/acpackage/example.py**
This module contains a function that adds two integers and returns the result. It includes a detailed docstring explaining parameters and examples.

```python
def add_num(a: int, b: int) -> int:
    """
    Add two numbers.

    Parameters
    ----------
    a : int
        The first number to be added.
    b : int
        The second number to be added.

    Returns
    -------
    int
        The sum of the two input numbers (a + b).

    Examples
    --------
    >>> add_num(2, 4)
    6
    >>> add_num(-2, 4)
    2
    """
    return a + b
```

**src/acpackage/`__init__.py`**
`__init__.py` acts as the "constructor" for a Python package, enabling its proper recognition, initialization, and controlled exposure of its contents.

```python
from .example import add_num
__all__ = ["add_num"]
```



# 2. Anatomy of `pyproject.toml`

Every modern Python package should include a pyproject.toml file. This file is the foundation and primary source of truth for package configuration. If it’s correct, your package will install, build, and work as expected with modern tools.
- You **must** include `[project]`, `[build-system]`, and the `name`, `version`, and `build-backend`.
- It's **highly recommended** to include `requires-python`.
- pyproject.toml is not just for builds, it’s a central configuration file for tools and environments.


```toml
[build-system]
requires = ["uv_build >= 0.8.20, <0.9.0"]
build-backend = "uv_build"

[project]
name = "acpackage"
description = "A package for performing basic addition"
readme = "README.md"
version = "0.0.1"
requires-python = ">=3.10"
license = { file = "LICENSE" }
keywords = []
authors = [
  { name = "Inessa Pawson", email = "inessa@albuscode.org" },
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Programming Language :: Python :: 3",
    "Operating System :: OS Independent"
]

dependencies = []

[project.urls]
Homepage = "https://github.com/albuscode/acpackage"
Documentation = ""
Issues = "https://github.com/albuscode/acpackage/issues"
Source = "https://github.com/albuscode/acpackage"
```



# 3. Build & Install Locally

### Download `acpackage` as .zip file: 
https://github.com/albuscode/acpackage/archive/refs/heads/main.zip

*Note:* Package names on Test PyPI must be unique. Rename the downloaded package to `acpackageYOURNAME`.

### Move to the package directory
```bash
cd acpackageYOURNAME
```

### Install build tools:
```bash
python -m pip install --upgrade build
```

### Build distributions:
```bash
python -m build
ls -lah dist
```
This produces both a source archive (*.tar.gz) and a wheel (*.whl) in dist/.

### Install the wheel to test:
```bash
pip install dist/acpackageYOURNAME-0.0.1-py3-none-any.whl
python -c "import acpackageYOURNAME; print(acpackageYOURNAME.add_num(2, 3))"
```

### Try **editable installs** (PEP 660):
```bash
pip install -e .
```
Editable mode is useful during development as you can change code without reinstalling. Not always supported by all build backends, but `uv_build` supports it.

# 4. Upload to TestPyPI

Create an account and API token at [TestPyPI](https://test.pypi.org/) , then upload with Twine.

```bash
pip install --upgrade twine
twine upload --repository testpypi dist/*
```


# 5. Install from TestPyPI & Verify

```bash
pip install --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple \
    acpackage-YOURNAME
python -c "import acpackageYOURNAME; print(acpackageYOURNAME.add_num(4, 5))"
```

If you exposed a console script:
```bash
acpackage-add 7 8
```

# 6. Metadata, Dependencies, & Extras

```toml
[project]
dependencies = ["simplejson>=3.19"]

[project.optional-dependencies]
dev = ["pytest", "ruff"]
cli = ["click>=8"]
```

Example CLI using Click:
```python
import sys
try:
    import click
except Exception:
    print("Install optional extra: pip install 'acpackage-yourname[cli]' ")
    raise

@click.command()
@click.argument("a", type=int)
@click.argument("b", type=int)
def main(a, b):
    from acpackage import add
    print(add(a, b))

if __name__ == "__main__":
    main()
```

# 7. Advanced Topics

## Namespace packages
- Implicit namespace packages (PEP 420).

## Binary extensions
- C/C++/Cython → platform wheels.

## Plugin entry points
- Use `project.entry-points` for discoverable plugins.



<!-- 08-workflows.md -->

# 8. Workflows & Best Practices

Example GitHub Actions release workflow:
```yaml
name: Release
on:
  push:
    tags:
      - 'v*'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.x'
      - run: python -m pip install --upgrade pip build twine
      - run: python -m build
      - run: twine check dist/*
      - env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
        run: twine upload dist/*
```

# 9. Resources

-  Packaging Python Projects by PyPA: packaging.python.org
-  Python Packaging 101 by PyOpenSci: https://www.pyopensci.org/python-package-guide/tutorials/intro.html