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.