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.tomlthat 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¶
- Why Packaging? - Reuse, distribution, professional practice
- Package Anatomy - Structure,
__init__.py, imports pyproject.toml- Describing your package- Exercise A - Package your Session 7 coffee shop code
- Exercise B - Package your Session 8
ConfigurationManagerasrobot_tools - The ROS2 Bridge - How Python packaging maps to ROS2
- Summary - What you've built across Sessions 7–9
Session Slides
View the presentation slides for this session:
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.pyandcoffee_shop_exceptions.pyfrom S7 - Your
configuration_manager.pyfrom 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
- Versioning —
robot_tools==1.0.0vsrobot_tools==1.1.0 - Dependency management — declare what your package needs (
pyyaml,lxml) - Distribution — share with teammates, publish to PyPI, use in ROS2
- Clean imports —
from robot_tools import ConfigurationManagernotimport 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:
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¶
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¶
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¶
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:
- Why packaging — reuse without copying, versioning, clean distribution
- Package structure —
__init__.py, modules, relative imports pyproject.toml— describing your package, declaring dependencies- Editable installs —
pip install -e .for development - Packaging S7 code — coffee shop as a proper installable library
- Packaging S8 code —
robot_toolswithConfigurationManager - The ROS2 connection —
package.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 →