Python Basics: Read Package Assets with importlib.resources
A packaged application may need a template or a small data file shipped beside its Python code. Building a path from the current working directory breaks when the caller runs elsewhere. importlib.resources accesses assets through the package loader, including when a package lives inside a ZIP file.
Prepare a scratch directory
These standalone examples were tested on Linux with Python 3.12.14. Use Python 3.9 or newer for files and as_file. Each example creates a tiny trusted ZIP package in its own temporary directory and removes it on exit. Use Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Run without -O so the example assertions remain enabled.
Create a fresh directory and keep using the same terminal:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Save each named file below as UTF-8. Do not name your script after a standard-library module.
1. Read text from a ZIP-backed package
Save as read_packaged_template.py:
from importlib.resources import files
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile
import sys
with TemporaryDirectory() as temp:
archive = Path(temp) / "assets.zip"
with ZipFile(archive, "w") as bundle:
bundle.writestr("report_assets_demo/__init__.py", "")
bundle.writestr("report_assets_demo/greeting.txt", "Hello, {name}!\n")
sys.path.insert(0, str(archive))
try:
template = files("report_assets_demo").joinpath("greeting.txt")
text = template.read_text(encoding="utf-8")
assert text == "Hello, {name}!\n"
print(text.format(name="Reader"), end="")
print("Resource is a file:", template.is_file())
finally:
sys.path.pop(0)Run it:
python3 -B read_packaged_template.pyOutput:
Hello, Reader!
Resource is a file: TrueThe resource is obtained from the named package, not from the terminal directory. read_text returns decoded content directly; it does not need a persistent filesystem path. These scripts create an empty init.py themselves, so importing the fixture does not execute unknown package code.
2. Enumerate only the assets you need
Save as list_packaged_assets.py:
from importlib.resources import files
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile
import sys
with TemporaryDirectory() as temp:
archive = Path(temp) / "assets.zip"
with ZipFile(archive, "w") as bundle:
bundle.writestr("report_assets_demo/__init__.py", "")
bundle.writestr("report_assets_demo/data/", "")
bundle.writestr("report_assets_demo/data/blue.txt", "blue")
bundle.writestr("report_assets_demo/data/green.txt", "green")
sys.path.insert(0, str(archive))
try:
folder = files("report_assets_demo").joinpath("data")
names = sorted(item.name for item in folder.iterdir()
if item.is_file() and item.name.endswith(".txt"))
assert names == ["blue.txt", "green.txt"]
print("Assets:", ", ".join(names))
print("Bytes:", folder.joinpath("blue.txt").read_bytes())
finally:
sys.path.pop(0)Run it:
python3 -B list_packaged_assets.pyOutput:
Assets: blue.txt, green.txt
Bytes: b'blue'The Traversable interface supplies iteration, child lookup, and reads without assuming an ordinary directory. Sort names when output order matters. A resource container can include Python modules and other files too, so this example selects only regular .txt assets inside its known data folder.
3. Adapt a resource to a path-only reader
Save as use_temporary_resource_path.py:
from importlib.resources import as_file, files
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile
import sys
def read_first_line(path):
with path.open(encoding="utf-8") as stream:
return stream.readline().rstrip("\n")
with TemporaryDirectory() as temp:
archive = Path(temp) / "assets.zip"
with ZipFile(archive, "w") as bundle:
bundle.writestr("report_assets_demo/__init__.py", "")
bundle.writestr("report_assets_demo/header.txt", "Quarterly report\n")
sys.path.insert(0, str(archive))
try:
resource = files("report_assets_demo").joinpath("header.txt")
with as_file(resource) as path:
print("Header:", read_first_line(path))
print("Path exists inside context:", path.is_file())
print("ZIP resource temporary path removed:", not path.exists())
assert not path.exists()
finally:
sys.path.pop(0)Run it:
python3 -B use_temporary_resource_path.pyOutput:
Header: Quarterly report
Path exists inside context: True
ZIP resource temporary path removed: Trueas_file yields a Path for APIs that insist on one. A ZIP resource is extracted temporarily and removed when the context closes, so complete the path-based operation inside the with block. A resource already on disk may not need extraction; do not generally assume that its path is removed afterward.
The demo packages are deliberately tiny and trusted. Resolving a package may import it, so a resource lookup is not a way to inspect arbitrary packages without executing their initialization. Use known resource names rather than paths supplied by an untrusted caller. Also verify that your build actually includes the assets: this API reads packaged files but does not add missing files to a wheel.
Clean up
In the same terminal, remove only the named sources, return to the original directory, and remove the empty scratch directory:
rm -- read_packaged_template.py list_packaged_assets.py use_temporary_resource_path.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf rmdir reports remaining files, inspect them instead of deleting unknown contents.
References
Acknowledgements
This post was created with assistance from AI.