importlib.resources provides a portable interface for files distributed inside a Python package. It avoids building paths from __file__ and can work with resources exposed by different import loaders.
Read packaged text
Assume package my_app includes data/config.json in its distribution:
from importlib.resources import files
import json
resource = files("my_app").joinpath("data", "config.json")
config = json.loads(resource.read_text(encoding="utf-8"))
print(config["name"])
The returned object is a Traversable, not a promise of pathlib.Path. Use read_text, read_bytes, iterdir, and joinpath for ordinary operations. If an external library requires a real path, use as_file() inside a with block and do not retain that path afterward.
from importlib.resources import as_file, files
resource = files("my_app").joinpath("model.bin")
with as_file(resource) as path:
load_model(path)
The resource must also be included in the wheel or sdist. Configure package data in your build system and test an installed artifact, not only the repository tree. The pyproject.toml guide helps organize that configuration.
The official importlib.resources documentation, accessed July 22, 2026, explains files, as_file, and support for resources that are not directly available on the filesystem.
Why paths are unreliable
A relative path is resolved from the process working directory, which changes between a local shell, a test runner, a service, and a container. Building a path beside __file__ still assumes that the importer exposes ordinary files. Python packages can also come from zip files or custom loaders. A resource API states the actual intention: read data owned by an importable package.
Pass a package name or module as the anchor. A module object remains easy to follow after refactoring:
from importlib.resources import files
from my_app import assets
template = files(assets).joinpath("welcome.html")
html = template.read_text(encoding="utf-8")
Treat names passed to joinpath() as trusted package-relative names. If users select a resource, map an allowed identifier to a known filename rather than accepting arbitrary path fragments.
Traverse without assuming pathlib
A Traversable exposes a small filesystem-like interface. Test is_file() or is_dir(), list children with iterdir(), and open streams with open(). Do not call methods available only on Path.
icons = files("my_app").joinpath("assets", "icons")
available = sorted(
item.name for item in icons.iterdir()
if item.is_file() and item.name.endswith(".svg")
)
Reading through open("rb"), read_bytes(), or read_text() avoids extraction. Use as_file() only when an external library requires a filename. The path it yields belongs to the context manager and may disappear afterward.
Include data in distributions
Presence in source control does not guarantee presence in a wheel. Configure package data for the chosen build backend, build both wheel and source distribution, inspect their contents, and install the wheel into a clean environment. Run the installed test from outside the repository so the source tree cannot hide a missing configuration rule. Check the sdist separately because downstream systems may build a wheel from it.
Resources suit read-only defaults, templates, schemas, certificates intended for distribution, and small reference datasets. They are not writable configuration. An installed package may be read-only or replaced during an upgrade. Copy an editable default into an application data directory first.
Handle missing and optional resources
Decide whether absence means a packaging defect or an optional feature. Required data should fail with useful context:
def load_query() -> str:
resource = files("my_app").joinpath("sql", "report.sql")
try:
return resource.read_text(encoding="utf-8")
except FileNotFoundError as exc:
raise RuntimeError("installed package lacks sql/report.sql") from exc
Do not silently use an empty schema or template. That delays failure and obscures its cause. Validate decoded JSON, TOML, or YAML separately from resource access so errors identify either packaging or content.
Test the resource contract
Unit tests can cover decoding, parsing, directory traversal, and error messages. An artifact test should build, install, import, and read every mandatory resource through importlib.resources. If zip-based execution is supported, exercise that importer too. Assert operations and content, not that the returned object is a Path.
For libraries supporting older Python releases, the importlib_resources backport may expose newer behavior. Keep the minimum version and import policy explicit. These checks make resources reliable across editable installs, wheels, operating systems, and import loaders.
Package and resource boundaries
The anchor passed to files() should identify the package that conceptually owns the data. A large application can place templates in my_app.templates, migrations in my_app.migrations, and sample data in my_app.examples. This keeps ownership visible and avoids a single miscellaneous directory. It also helps build configuration include only intentional files.
Resource names are not import names. Dots in a filename do not create modules, and a subdirectory is not automatically an importable package. Start from a known package and traverse its children. When a library offers plugins, do not inspect arbitrary installed packages for data. Define a documented plugin interface and let each plugin expose its own anchor.
The older functional helpers such as read_text() and open_binary() still appear in existing code, but the files() API composes better for nested resources and new development. Check the Python versions supported by the project before adopting parameters introduced in newer releases. A compatibility decision belongs in project policy, not in scattered exception handlers.
Text encoding and structured formats
Always specify an encoding for text. UTF-8 is the usual package-data choice, and declaring it prevents platform defaults from changing behavior. Resource access returns bytes or text; parsing is a separate responsibility. Catch a JSON decoding error differently from a missing file because the fixes differ: one means malformed distributed content, the other often means incorrect build configuration.
Large datasets are usually a poor fit for package resources. They increase every installation, update, and cache footprint. Consider an optional distribution, a download with integrity verification, or an application-managed data location. Conversely, a small schema required for basic correctness should ship with the package rather than depend on the network.
Temporary paths and cleanup
as_file() may materialize data into a temporary location. Finish all path-based operations inside its context, including lazy reads by third-party libraries. If a library retains the path for later, copy the resource into an application-controlled temporary directory with a clearly defined lifetime. Do not return the context-managed path from a helper.
Nested contexts are rarely necessary. Resolve the resource once, enter as_file(), perform the integration, and release it. This structure also makes cleanup behavior clear in tests. If the process crashes, application-owned temporary files need a cleanup policy; resources managed directly by as_file() remain the library's responsibility.
A practical review checklist
Before release, confirm that each anchor imports from the installed artifact, every required filename is present in both wheel and sdist, text uses an explicit encoding, and no caller assumes Path. Verify that writable state lives outside the package. Test missing and malformed content, plus any external library used through as_file().
Finally, document whether resource names are stable public API. Renaming an internal template is harmless when only package code references it, but it is a breaking change when users call files() directly. A small public helper can hide the layout and provide a more durable interface.