Skip to content

Package

Learning outcomes

  • Understand how to package a Python project
  • Package our Python project locally
  • Run a script with our Python project
  • Upload our package to PyPI
For teachers

Prerequisites are:

  • .

Teaching goals are:

  • .

Prior:

  • .

Lesson plans:

gantt
  title Lesson plan pair programming 1
  dateFormat X
  axisFormat %s
  Introduction : intro, 0, 5s
  Theory 1: theory_1, after intro, 5s
  Exercise 1: crit, exercise_1, after theory_1, 15s
  Feedback 1: feedback_1, after exercise_1, 5s
  Theory 2: theory_2, after feedback_1, 5s
  Exercise 2: crit, exercise_2, after theory_2, 10s
  Exercise 2 after the break: crit, after exercise_2, 5s

Why put your code in a Python package?

Our app on PyPI

bacsim on PyPI

An earlier project called bacsim on (the test version of) PyPI

You want to share or re-use your work in the regular way.

Exercises

Exercise 1: install package locally

Answer

Yes.

The learners' project already follows all of these guidelines :-)

  • Does the learners' project check if our project can be put in a package?
Answer

Yes.

You can find this in the .github/workflows/check_package.yaml script

  • How does the learners' project put our code into a package? You can find it in the CI script.
Answer

The script uses:

python3 -m pip install .
  • Create a local package from the learners' project's code. Use the CI script's code as a hint.
Answer

The script uses:

python3 -m pip install .

However, variations to get this to work on your computer are:

  • use python instead of python3
  • add --break-system-packages at the end, i.e. run python3 -m pip install . --break-system-packages
  • Scan the file main.py in the root folder of the learners' project. Confirm that it assumes our Python package to be present. What is the name of our Python package?
Answer

The script imports a function from weather:

from weather.reader import (
    read_data,
)

Hence, our package is called weather.

Another way to find out, is to read pyproject.toml, another file in the root folder of our project. It reads:

[project]
name = "weather"
  • Run the script main.py to verify our package is installed. Have you successfully installed our code as a package?
How to run that script again?

Run:

python main.py
Answer

If you see this error message, the answer is 'no':

Traceback (most recent call last):
  File "/home/richel/GitHubs/programming_formalisms_project_summer_2026/main.py", line 10, in <module>
    from weatherx.experiment import (
ModuleNotFoundError: No module named 'weather'

If you get an error such as Cannot find file 'parameters.csv' it means that our package is installed successfully. Sure, it does not do actual work, but it is installed!

Exercise 2: upload package to PyPI

Exercise 2.1: Build the package distribution files

In this exercise, we are going to build the package. Building a package means collecting all the files needed to be able to distribute it.

The most important file for this is a file called pyproject.toml, which describes which files to use in the package. pyproject.toml is called the package manifest. The toml file extension hints that this file is written in TOML, a markup language.

Take a look at that file.

How does that file look?

The file will look similar to this:

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

[tool.hatch.build.targets.wheel]
packages = ["weather"]

[project]
name = "uppsalaweathersummer2026"
version = "0.1"
authors = [
  { name="Richèl Bilderbeek", email="rjcbilderbeek@gmail.com" },
]
description = "The Programming Formalisms course learners project"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]


[project.urls]
"Homepage" = "https://github.com/programming-formalisms/programming_formalisms_project_summer_2026"
"Bug Tracker" = "https://github.com/programming-formalisms/programming_formalisms_project_summer_2026/issues"

[tool.ruff]
    lint.select = ["ALL"]
    lint.ignore = ["ANN", "PT", "PTH", "D211", "D213", "S101", "B006"]

[tool.hatch.build.targets.sdist]
include = [
  "/data",
  "/weather",
  "/tests",
]

What is the version of our package?

Answer

The version can be found at the project section, for example:

[project]
...
version = "0.1"

In this case, the version is 0.1.

Will the work in the learners folder be part of the package? Why?

Answer

No.

We can see in the section below, that only the data, weather and tests folders become part of our package:

[tool.hatch.build.targets.sdist]
include = [
  "/data",
  "/weather",
  "/tests",
]

From our package manifest, we need to install the Python package to build our Python package. For this, we follow the steps at the official Python packaging documentation:

Install the build Python package:

python3 -m pip install --upgrade build --break-system-packages

Before building, we need to modify the pyproject.toml file, so that all learners have a unique project and all can upload it to PyPI.

In pyproject.toml, modify the project name to something unique, e.g:

[project]
name = "uppsalaweathersummer2026sven"

Run build in the root folder of our package to build our unique package:

python3 -m build
How does that look like?
richel@richel-N141CU:~/GitHubs/programming_formalisms_project_summer_2026$ python3 -m build
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for wheel...
* Building wheel...
Successfully built weather-1.0.1.tar.gz and weather-1.0.1-py3-none-any.whl

Now there will be files in the dist folder. These are the file to distribute your files.

How does that look like?
richel@richel-N141CU:~/GitHubs/programming_formalisms_project_summer_2026$ ls dist/
weather-1.0.1-py3-none-any.whl  weather-1.0.1.tar.gz

Well done, you've just created the package distribution files!

Exercise 2.2: Upload our package distribution

Last step is to upload our package distribution files to https://test.pypi.org:

Install the twine package to be able to upload our package distribution files:

python3 -m pip install --upgrade twine --break-system-packages

After registering to https://test.pypi.org, you have the API token be allowed to upload to testPyPI. Upload:

python3 -m twine upload --repository testpypi dist/*
I got a 403 error with the word 'legacy' in it

This error happens when you are not the first to upload this package.

To solve this, edit the pyproject.toml file. Look for this section:

[project]
name = "uppsalaweather"

Replace the name by something new, for example:

[project]
name = "uppsalaweather_sven"

Rebuild the package again and upload.

How does that look like?
richel@richel-N141CU:~/GitHubs/programming_formalisms_project_summer_2026$ python3 -m twine upload --repository testpypi dist/*
Uploading distributions to https://test.pypi.org/legacy/
Enter your API token:
Uploading weather-1.0.1-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 24.8/24.8 kB  00:00  72.3 MB/s
Uploading weather-1.0.1.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB  00:00  8.5 MB/s

View at:
https://test.pypi.org/project/weather/1.0.1/

Indeed, the package can now be found at https://test.pypi.org/project/weather/1.0.1/.

How does that look like?

bacsim on PyPI

Confirm that the package works.

'No matching distribution found'?

This happens if you are too quick:

richel@richel-latitude-7430:~/GitHubs/programming_formalisms_project_summer_2026$ pip install -i https://test.pypi.org/simple/ uppsalaweathersummer2026richel==0.3 --break-system-packages
Defaulting to user installation because normal site-packages is not writeable
WARNING: Skipping /usr/lib/python3.12/dist-packages/protontricks-1.10.5.dist-info due to invalid metadata entry 'name'
Looking in indexes: https://test.pypi.org/simple/
ERROR: Could not find a version that satisfies the requirement uppsalaweathersummer2026richel==0.3 (from versions: 0.1, 0.2)
ERROR: No matching distribution found for uppsalaweathersummer2026richel==0.3

Wait around 3 seconds and try again 👍

import weather.analysis
weather.analysis.do_analysis()
I get an assertion error?

This happens when there are failing assert in the package, for example:

richel@richel-latitude-7430:~/GitHubs$ python
Python 3.12.3 (main, Mar 23 2026, 19:04:32) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import weather.analysis
Anna's function has read the data
Sven's function has created statistics output
Sven's function has created a figure
Analysis done
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/richel/.local/lib/python3.12/site-packages/weather/analysis.py", line 67, in <module>
    assert check_file_exists("main.py")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

To fix this, run Python in release mode:

python -O

Now the asserts are removed.

How does this look like?

Similar to this:

richel@richel-latitude-7430:~/GitHubs$ python -O
Python 3.12.3 (main, Mar 23 2026, 19:04:32) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import weather.analysis
Anna's function has read the data
Sven's function has created statistics output
Sven's function has created a figure
Analysis done
>>> weather.analysis.do_analysis()
Anna's function has read the data
Sven's function has created statistics output
Sven's function has created a figure
Analysis done
>>>