Skip to content
← All Python articles

Python explainer

Test the Wheel You Ship, Not Your Checkout

About 3 min to read. Allow extra time to work through examples.

Find the data file a wheel leaves out. Then catch missing files and missing type information by testing the installed wheel in a clean environment.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your tests pass, and you publish version 0.1.0. The first user who calls greet() gets this error, shown here with the path shortened:

FileNotFoundError: [Errno 2] No such file or directory: '.../site-packages/yourpkg/greeting.txt'

Your tests import the package from your project folder, where greeting.txt sits next to the code. The wheel, the zip file that pip install unpacks, left the file out. In this article, you build that package, find the gap and fix it. Then you test the wheel the way a user gets it.

The grader runs one file at a time, so it cannot run this walkthrough. Follow it on your own machine, with Python 3.12 or later and the build package (pip install build). The shell commands are for Linux and macOS.

Make a package that reads a data file

package-demo/
    pyproject.toml
    src/
        yourpkg/
            __init__.py
            greeting.txt
            py.typed

greeting.txt holds one line, Hello, {name}!, and py.typed is an empty file that tells type checkers to read the package's annotations. Put this in src/yourpkg/__init__.py:

from importlib.resources import files

__all__ = ["greet"]


def greet(name: str) -> str:
    template = files("yourpkg").joinpath("greeting.txt").read_text(encoding="utf-8")
    return template.strip().format(name=name)

importlib.resources.files finds the file inside the installed package, wherever that is. Then add pyproject.toml:

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "package-lesson-demo"
version = "0.1.0"
requires-python = ">=3.12"

[tool.setuptools.packages.find]
where = ["src"]

The src folder keeps yourpkg out of the project root, so Python started there cannot import your source by accident.

Build the wheel and look inside

python -m build
python -m zipfile -l dist/package_lesson_demo-0.1.0-py3-none-any.whl

The listing has yourpkg/__init__.py and yourpkg/py.typed, but no greeting.txt. Setuptools packs Python files and py.typed for you, but not other data files. Name them in pyproject.toml:

[tool.setuptools.package-data]
yourpkg = ["*.txt"]

Build again, and the listing includes yourpkg/greeting.txt. python -m build builds the source archive first and then builds the wheel from that archive, not from your folder. A file that the archive misses goes missing from the wheel too.

Install the wheel somewhere clean

Make a new virtual environment outside the project, and install the wheel into it:

python -m venv /tmp/smoke
/tmp/smoke/bin/python -m pip install dist/package_lesson_demo-0.1.0-py3-none-any.whl
cd /tmp/smoke

Save this as consumer.py in /tmp/smoke:

from typing import assert_type

import yourpkg
from yourpkg import greet

message = greet("Ada")
assert_type(message, str)
assert message == "Hello, Ada!"
print("imported from:", yourpkg.__file__)

Run it, then type-check it against the installed package, using mypy from your development environment:

/tmp/smoke/bin/python -I consumer.py
mypy --strict --python-executable /tmp/smoke/bin/python consumer.py

The printed path must be inside /tmp/smoke. The -I flag stops Python adding the script's folder and PYTHONPATH to the import path, so nothing can reach your source folder by mistake. --python-executable makes mypy look for packages where that Python finds them.

When mypy cannot see the types

If py.typed is missing from the wheel, mypy prints these two errors:

consumer.py:3: error: Skipping analyzing "yourpkg": module is installed, but missing library stubs or py.typed marker  [import-untyped]
consumer.py:7: error: Expression is of type "Any", not "str"  [assert-type]

The second error comes from assert_type. Without the marker, every name from yourpkg has the type Any, which hides mistakes instead of reporting them. Put py.typed inside the package folder, next to __init__.py, not at the project root. If the marker is in the wheel and mypy still complains, check that --python-executable points at the environment you installed into.

Keep the check for every release

Run the same install check in CI for each Python version you support. If the package ships templates or other data, call the code that reads them in consumer.py too.

step 16 of 16

Config, Logs and Untrusted Input

You've reached the last step

Revisit the track to review its lessons and exercises, or explore another topic.

Explore more tracks