Python applications often need to ship files next to their code: JSON models, HTML templates, test certificates, SQL schemas, translation catalogs, examples, images, or static datasets. A common mistake is to assume those resources always live in a normal directory and build paths from __file__. That may work during development, but it can fail after installation as a wheel, when code is loaded from a zip archive, or when a custom importer does not expose ordinary files. Python importlib.resources provides the official way to access resources that belong to importable packages.
This guide explains how to locate, read, and temporarily materialize package resources, how to include them in distributions, how to test the final wheel, and how to avoid path traversal and lifetime mistakes. It complements our guides to Python importlib, Python pathlib, Python zipfile, Python tempfile, and Python shlex.
Why __file__ is not enough
A path based on Path(__file__).parent assumes that the module has a physical file and that the resource sits beside it. That is not guaranteed. Packages can be imported from archives, embedded applications, frozen bundles, or alternative loaders. Paths based on the current working directory are even less reliable because they depend on how the program was launched.
from pathlib import Path
# Fragile in some distribution scenarios
config_path = Path(__file__).parent / "data" / "config.json"importlib.resources asks the import system for the resource instead of guessing a physical layout.
Package layout
my_package/
__init__.py
reader.py
data/
config.json
message.txtThe files must be included in the built distribution. The resource API cannot read a file that the build backend omitted. Configure package data in pyproject.toml and inspect the generated wheel before release.
Start with files()
The modern interface begins with importlib.resources.files(). It returns a traversable object representing a package or module.
from importlib.resources import files
root = files("my_package")
resource = root.joinpath("data", "config.json")
print(resource)A traversable object may map to a real path or a virtual resource. Keep using its methods instead of converting it immediately to Path.
Read text safely
text = (
files("my_package")
.joinpath("data", "message.txt")
.read_text(encoding="utf-8")
)Always declare the encoding. UTF-8 avoids platform-specific defaults. JSON can be loaded after reading the text.
import json
data = json.loads(
files("my_package")
.joinpath("data", "config.json")
.read_text(encoding="utf-8")
)Read binary data
Images, compressed models, and other binary formats should use read_bytes().
content = files("my_package").joinpath("data", "logo.png").read_bytes()Do not decode arbitrary binary data as text. The API returns bytes but does not validate the file format.
Use a package object as the anchor
import my_package
from importlib.resources import files
root = files(my_package)Passing the imported package avoids string typos and makes the dependency explicit.
Check resource type
resource = files("my_package").joinpath("data", "config.json")
if not resource.is_file():
raise FileNotFoundError("required config.json is missing")Use is_file() and is_dir() when absence is expected and deserves a custom message. Required resources should fail early so packaging errors are easy to diagnose.
List packaged resources
directory = files("my_package").joinpath("data")
for item in directory.iterdir():
print(item.name, item.is_file())Listing does not make external input safe. If a user chooses a resource, map the request to an allowlist rather than appending arbitrary path components.
Prevent path traversal
ALLOWED = {
"default": "config.json",
"test": "config-test.json",
}
filename = ALLOWED.get(option)
if filename is None:
raise ValueError("unknown option")
resource = files("my_package").joinpath("data", filename)This prevents values such as ../secret from changing the intended location.
When a library requires a real path
Some older libraries accept only filesystem paths. Use as_file() to obtain a temporary physical path when necessary.
from importlib.resources import as_file, files
resource = files("my_package").joinpath("data", "model.bin")
with as_file(resource) as path:
load_model(str(path))The path may disappear after the context ends. Do not cache or return it for later use. Complete all path-dependent work inside the with block.
Materialize directories carefully
Recent Python versions can materialize traversable directories in supported scenarios.
templates = files("my_package").joinpath("templates")
with as_file(templates) as template_path:
renderer.load_directory(template_path)Treat the directory as temporary and avoid assumptions about its location.
Version compatibility
The modern API evolved across Python releases. Projects supporting older versions can use the importlib_resources backport.
try:
from importlib.resources import files, as_file
except ImportError:
from importlib_resources import files, as_fileCentralize this compatibility layer in one module. Review the official importlib.resources documentation and the Python packaging guide for details that match your minimum Python version and build backend.
Include resources in the wheel
A resource working in the repository does not prove that it reached the distribution artifact. Build the wheel, inspect it, install it in a clean environment, and run tests against the installation.
python -m build
python -m zipfile -l dist/my_package-1.0.0-py3-none-any.whlThis catches missing package data and backend configuration errors before users encounter them.
Resources versus editable configuration
Packaged resources are appropriate for immutable defaults. User-editable configuration belongs outside the installed package, in an application data directory, environment variables, or a configuration service. Writing inside site-packages is unreliable and may require elevated permissions.
Do not write to package resources
The resource API is designed for reading. Copy a default file to a writable location before modifying it.
from pathlib import Path
from importlib.resources import files
source = files("my_package").joinpath("data", "default.json")
target = Path.home() / ".my_app" / "default.json"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(source.read_bytes())Automated tests
def test_packaged_config():
resource = files("my_package").joinpath("data", "config.json")
assert resource.is_file()
data = json.loads(resource.read_text(encoding="utf-8"))
assert "version" in dataAdd an integration test that builds and installs the wheel in a temporary environment. That test validates the artifact users actually receive.
Error handling
def load_config():
resource = files("my_package").joinpath("data", "config.json")
try:
return json.loads(resource.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise RuntimeError("the package was installed without config.json") from exc
except json.JSONDecodeError as exc:
raise RuntimeError("the packaged config.json is invalid") from excTranslate low-level failures into messages that explain whether the installation is incomplete or the content is malformed.
Performance and caching
Small immutable resources can be loaded once with functools.cache.
from functools import cache
@cache
def load_schema():
return files("my_package").joinpath("data", "schema.json").read_text(encoding="utf-8")Do not cache very large files blindly. Measure memory use and consider streaming APIs when available.
Common mistakes
- Building paths from the current working directory.
- Assuming every resource has a permanent physical path.
- Forgetting package data in the wheel.
- Keeping an
as_file()path after the context closes. - Appending untrusted input to
joinpath(). - Trying to modify installed package resources.
- Testing only from the source tree.
- Reading text without an explicit encoding.
Best practices
- Use
files()as the primary entry point. - Read text with explicit UTF-8.
- Use
as_file()only when another API requires a path. - Keep the temporary path inside its context.
- Validate resources in the built wheel.
- Separate immutable defaults from user configuration.
- Restrict resource names derived from external input.
- Test every supported Python version.
Conclusion
Python importlib.resources provides a reliable abstraction for files distributed with packages without depending on fragile physical paths. Traversable resources work across standard installations, wheels, and alternative import loaders, while as_file() bridges libraries that still require a real path.
Correct packaging remains essential: include the data, inspect the wheel, and test the installed artifact. With these practices, templates, schemas, and default assets remain predictable in development and production.






