Skip to content
Python Basics: Read Package Assets with importlib.resources

Python Basics: Read Package Assets with importlib.resources

October 23, 2026

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 1

Save 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.py

Output:

Hello, Reader!
Resource is a file: True

The 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.py

Output:

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.py

Output:

Header: Quarterly report
Path exists inside context: True
ZIP resource temporary path removed: True

as_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_dir

If rmdir reports remaining files, inspect them instead of deleting unknown contents.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on