Skip to content

Session 9: Python Packaging & Module Distribution

Week: 9 Element: ICTPRG430 Element 2.2 Duration: 4 hours Phase: Advanced OOP - Professional Practice


Session Introduction

You've spent two sessions writing clean, modular, professional-grade Python code. In Session 7 you refactored messy procedural code into a well-structured OOP system. In Session 8 you built a ConfigurationManager as a standalone module with its own exception hierarchy and logging.

Now it's time to answer the question those sessions were building towards: how do you share, reuse, and distribute that code?

This session introduces Python packaging — the mechanism that turns your modules into proper, installable libraries. You'll package your Session 7 coffee shop code and your Session 8 ConfigurationManager into real pip-installable packages.

This also sets you up directly for Session 10. ROS2 — the robotics middleware you'll use for the rest of this course — is built entirely on the same packaging concept. Once you understand Python packages, ROS2's package.xml and colcon build will make immediate sense.

Not Assessable

This session is hands-on practice. There is no AT2 submission. Focus on understanding the concepts — you'll use them every time you work in ROS2.


Learning Objectives

By the end of this session, you will be able to:

  • Explain what a Python package is and why packaging matters
  • Restructure existing modules into a proper package directory layout
  • Write a pyproject.toml that describes your package
  • Install a package locally in editable mode with pip install -e .
  • Import from your package using clean, professional import paths
  • Explain the connection between Python packaging and ROS2's package system

Session Structure

  1. Why Packaging? - Reuse, distribution, professional practice
  2. Package Anatomy - Structure, __init__.py, imports
  3. pyproject.toml - Describing your package
  4. Exercise A - Package your Session 7 coffee shop code
  5. Exercise B - Package your Session 8 ConfigurationManager as robot_tools
  6. The ROS2 Bridge - How Python packaging maps to ROS2
  7. Summary - What you've built across Sessions 7–9

Session Slides

View the presentation slides for this session:

📊 Open Slides in New Window

The slides contain: - Why packaging matters for robotics - Package anatomy and __init__.py - pyproject.toml walkthrough - Coffee shop packaging exercise - robot_tools package exercise - The ROS2 packaging connection


Pre-Session Preparation

Required

  • Python 3.9+ installed
  • VS Code with Python extension
  • Completed Session 7 (coffee shop refactoring deliverables)
  • Completed Session 8 (ConfigurationManager class)

Review

  • Module imports (from module import Class)
  • Your coffee_shop_refactored.py and coffee_shop_exceptions.py from S7
  • Your configuration_manager.py from S8

1. Why Packaging?

The Problem Without Packages

Right now, your code lives in individual files. To reuse ConfigurationManager in another project you'd copy the file across. To use your coffee shop exceptions in a new app you'd copy them again. That's how duplication starts.

project_a/
├── configuration_manager.py   ← copy
├── robot_base.py

project_b/
├── configuration_manager.py   ← another copy, possibly out of date!
├── sensor_reader.py

The Solution: A Package

A package is code that can be installed once and imported anywhere:

# After packaging and pip install:
from robot_tools import ConfigurationManager
from coffee_shop.exceptions import InsufficientPointsError

No copying. No drift between versions. Exactly how the Python ecosystem (and ROS2) works.

Why Professionals Package Code

  • Reuse — install once, use in any project
  • Versioningrobot_tools==1.0.0 vs robot_tools==1.1.0
  • Dependency management — declare what your package needs (pyyaml, lxml)
  • Distribution — share with teammates, publish to PyPI, use in ROS2
  • Clean importsfrom robot_tools import ConfigurationManager not import sys; sys.path.append(...)

2. Package Anatomy

The Simplest Package

A package is just a directory with an __init__.py file:

coffee_shop/
├── __init__.py        ← this makes it a package
├── models.py          ← MenuItem, Order, Customer, LoyaltyProgram
└── exceptions.py      ← CoffeeShopError hierarchy

__init__.py — The Package Entry Point

__init__.py controls what gets exposed when someone imports your package:

# coffee_shop/__init__.py

# Option 1: Empty — users import from submodules
# from coffee_shop.models import MenuItem

# Option 2: Re-export the most useful things
from .models import MenuItem, Order, Customer, LoyaltyProgram
from .exceptions import CoffeeShopError, OrderError, InsufficientPointsError

__version__ = "1.0.0"
__author__ = "Your Name"

With Option 2, users can write:

from coffee_shop import MenuItem        # Clean!
from coffee_shop import InsufficientPointsError

Relative vs Absolute Imports

Inside a package, use relative imports (. means "this package"):

# Inside coffee_shop/models.py
from .exceptions import OrderError      # Relative — preferred inside a package

3. pyproject.toml — Describing Your Package

pyproject.toml is the modern way to describe a Python package. It lives at the project root (one level above your package folder):

my_project/           ← project root
├── pyproject.toml    ← package description
├── README.md
└── coffee_shop/      ← the actual package
    ├── __init__.py
    ├── models.py
    └── exceptions.py

Minimal pyproject.toml

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "coffee-shop"
version = "1.0.0"
description = "Coffee shop ordering system — OOP refactoring example"
requires-python = ">=3.9"

[project.optional-dependencies]
dev = ["pytest"]

With External Dependencies

[project]
name = "robot-tools"
version = "1.0.0"
description = "Robot configuration and utilities"
requires-python = ">=3.9"
dependencies = [
    "pyyaml>=6.0",
]

Installing Locally in Editable Mode

# From the project root (where pyproject.toml lives):
pip install -e .

The -e flag means editable — changes to your source files are immediately reflected without reinstalling. This is how you work during development.


4. Exercise A: Package the Coffee Shop

What You Have (From Session 7)

coffee_shop_refactored.py   ← MenuItem, Order, Customer, LoyaltyProgram
coffee_shop_exceptions.py   ← CoffeeShopError hierarchy
test_coffee_shop.py         ← Unit tests

Target Structure

Reorganise into this layout:

coffee_shop_package/          ← project root
├── pyproject.toml
├── README.md
└── coffee_shop/              ← the package
    ├── __init__.py
    ├── models.py             ← move classes from coffee_shop_refactored.py
    └── exceptions.py         ← move from coffee_shop_exceptions.py

Step 1: Create the directory structure

mkdir coffee_shop_package
cd coffee_shop_package
mkdir coffee_shop
touch coffee_shop/__init__.py
touch pyproject.toml

Step 2: Move your classes

Move MenuItem, Order, Customer, LoyaltyProgram into coffee_shop/models.py. Move your exception hierarchy into coffee_shop/exceptions.py. Update internal imports to use relative imports.

Step 3: Write __init__.py

# coffee_shop/__init__.py
from .models import MenuItem, Order, Customer, LoyaltyProgram
from .exceptions import (
    CoffeeShopError,
    OrderError,
    InvalidItemError,
    PaymentError,
    InsufficientPointsError,
)

__version__ = "1.0.0"

Step 4: Write pyproject.toml

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "coffee-shop"
version = "1.0.0"
description = "Coffee shop ordering system — OOP refactoring example"
requires-python = ">=3.9"

Step 5: Install and test

pip install -e .
python -c "from coffee_shop import MenuItem; print('Package works!')"

Step 6: Verify clean imports

from coffee_shop import MenuItem, Order
from coffee_shop.exceptions import InsufficientPointsError

item = MenuItem("Latte", 4.50, ['small', 'medium', 'large'])
print(item.calculate_price('small'))   # 4.50

5. Exercise B: Package robot_tools

Now package your Session 8 ConfigurationManager as a robot_tools library. This is the robotics-relevant version — and the exact pattern you'll see in ROS2.

Target Structure

robot_tools_package/
├── pyproject.toml
├── README.md
└── robot_tools/
    ├── __init__.py
    └── configuration.py      ← ConfigurationManager + exception hierarchy

robot_tools/__init__.py

from .configuration import (
    ConfigurationManager,
    ConfigurationError,
    ConfigFileNotFoundError,
    ConfigParseError,
    ConfigValidationError,
)

__version__ = "1.0.0"

pyproject.toml

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "robot-tools"
version = "1.0.0"
description = "Robot configuration utilities"
requires-python = ">=3.9"
dependencies = [
    "pyyaml>=6.0",
]

Install and use

pip install -e .
from robot_tools import ConfigurationManager, ConfigFileNotFoundError

config = ConfigurationManager('robot_config.json')
name = config.get('name')

6. The ROS2 Bridge

Python Packaging → ROS2 Packaging

ROS2 uses the same packaging concept with slightly different tooling:

Python ROS2
pyproject.toml package.xml
pip install -e . colcon build
from robot_tools import ... from my_ros_pkg import ...
pip colcon / apt
Virtual environment ROS2 workspace (install/)

A ROS2 Package Looks Familiar

my_ros2_package/
├── package.xml          ← like pyproject.toml
├── setup.py
└── my_ros2_package/
    ├── __init__.py      ← same!
    └── robot_node.py
<!-- package.xml -->
<package format="3">
  <name>my_ros2_package</name>
  <version>1.0.0</version>
  <description>Robot controller node</description>
  <depend>rclpy</depend>
</package>

You Already Know This

When you run colcon build in a ROS2 workspace in Session 10, you're doing the same thing as pip install -e . — building and installing a package so it can be imported. The concepts are identical. Only the tools differ.


Check Your Understanding

Q1: What makes a directory a Python package?

Answer

The presence of an __init__.py file. Without it, Python treats the directory as a plain folder, not an importable package.

Q2: What does pip install -e . do differently from pip install .?

Answer

The -e (editable) flag installs the package as a link to your source files rather than copying them. Changes you make to the source are immediately reflected without reinstalling — essential during development.

Q3: Why use relative imports inside a package?

Answer

Relative imports (from .models import ...) are explicit about the package boundary. They work correctly regardless of where the package is installed and make it clear the import is from within the same package.

Q4: What's the ROS2 equivalent of pyproject.toml?

Answer

package.xml describes the ROS2 package metadata and dependencies, just as pyproject.toml does for Python packages. colcon build processes it the same way pip install processes pyproject.toml.


Session Summary

You've learned:

  1. Why packaging — reuse without copying, versioning, clean distribution
  2. Package structure__init__.py, modules, relative imports
  3. pyproject.toml — describing your package, declaring dependencies
  4. Editable installspip install -e . for development
  5. Packaging S7 code — coffee shop as a proper installable library
  6. Packaging S8 coderobot_tools with ConfigurationManager
  7. The ROS2 connectionpackage.xml + colcon build = same pattern

The Thread Through Sessions 7–9

Session What You Built Why It Matters
S7 Clean modular OOP code Code worth packaging
S8 ConfigurationManager as standalone module Reusable component
S9 Packaged both as installable libraries Professional distribution
S10 ROS2 packages You already understand this

Navigation: ← Week 8 | Learning Plan | Week 10 →