Typorithm · medium

Package resources inside a ZIP import

pythonlibrariespackagingindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Package resources inside a ZIP import

0%

Concept and contract

Read package data without assuming every imported module lives in an ordinary directory.

A package resource is data associated with an importable package. Its location need not be a normal filesystem directory: the package may be imported directly from an archive. Constructing a nearby path from __file__ therefore makes a stronger assumption than many reusable libraries should require.

The example creates an owned ZIP containing a package and a JSON defaults resource. It imports that package from the archive and reads through importlib.resources.files. The returned Traversable describes access to the resource without promising that the data already has a permanent ordinary filesystem path.

Mechanism and worked example

The driver writes both package files into owned.zip, temporarily adds that archive to the import path and imports the named fixture.

The driver writes both package files into owned.zip, temporarily adds that archive to the import path and imports the named fixture. Files(package).joinpath selects defaults.json, and read_text obtains the JSON document using UTF-8. The parsed minimum is returned as an ordinary dictionary.

The finally block removes the temporary import-path entry and cached package reference used by the experiment. This keeps later cases from accidentally observing an earlier archive. It is fixture cleanup, not a recipe for unloading arbitrary production modules while other components still hold references to them.

Complete worked example

python

import importlib
from importlib.resources import files, as_file
import json
from pathlib import Path
import sys
from zipfile import ZipFile

def inspect_example(minimum):
    archive = Path('owned.zip').resolve()
    package_name = 'owned_resource_demo'
    with ZipFile(archive, 'w') as bundle:
        bundle.writestr(package_name + '/__init__.py', '')
        bundle.writestr(package_name + '/defaults.json', json.dumps({'minimum': minimum}))
    sys.path.insert(0, str(archive))
    importlib.invalidate_caches()
    try:
        package = importlib.import_module(package_name)
        resource = files(package).joinpath('defaults.json')
        settings = json.loads(resource.read_text(encoding='utf-8'))
        return settings
    finally:
        sys.path.remove(str(archive))
        sys.modules.pop(package_name, None)
        sys.path_importer_cache.pop(str(archive), None)
Reasoning
The driver writes both package files into owned.zip, temporarily adds that archive to the import path and imports the named fixture. Files(package).joinpath selects defaults.json, and read_text obtains the JSON document using UTF-8. The parsed minimum is returned as an ordinary dictionary.
Download the reusable Python and packaging computer lab ↗

Focused decision: use the complete example for context

python

resource = files(package).joinpath('defaults.json')
settings = json.loads(resource.read_text(encoding='utf-8'))
Decision
Read packaged data through a Traversable, then materialize it only inside an as_file context when a real path is needed.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The counterexample treats the package's __file__ location as an ordinary directory and appends defaults.json.

The counterexample treats the package's __file__ location as an ordinary directory and appends defaults.json. For a ZIP-backed import, the resulting path traverses through an archive file as though it were a directory. The verified macOS environment raises NotADirectoryError.

That error is useful evidence of the mistaken assumption, not a resource intentionally omitted from the package. Other import mechanisms may fail with another filesystem error. Keep the domain lesson focused on the unsupported path assumption rather than catching a specific operating-system message and pretending the resource was successfully loaded.

Controlled bug: the executable contract rejects this version

python

import importlib
from importlib.resources import files, as_file
import json
from pathlib import Path
import sys
from zipfile import ZipFile

def inspect_example(minimum):
    archive = Path('owned.zip').resolve()
    package_name = 'owned_resource_demo'
    with ZipFile(archive, 'w') as bundle:
        bundle.writestr(package_name + '/__init__.py', '')
        bundle.writestr(package_name + '/defaults.json', json.dumps({'minimum': minimum}))
    sys.path.insert(0, str(archive))
    importlib.invalidate_caches()
    try:
        package = importlib.import_module(package_name)
        resource = Path(package.__file__).parent / 'defaults.json'
        settings = json.loads(resource.read_text(encoding='utf-8'))
        return settings
    finally:
        sys.path.remove(str(archive))
        sys.modules.pop(package_name, None)
        sys.path_importer_cache.pop(str(archive), None)
Reasoning
The counterexample treats the package's __file__ location as an ordinary directory and appends defaults.json. For a ZIP-backed import, the resulting path traverses through an archive file as though it were a directory. The verified macOS environment raises NotADirectoryError.
Download the reusable Python and packaging computer lab ↗

Independent practice

Add an observation showing that as_file materializes the selected resource as a real file during its context.

Add an observation showing that as_file materializes the selected resource as a real file during its context. Keep reading the settings through the resource API and retain the returned minimum. The first hint is to enter as_file with the existing Traversable.

The second hint is to check the yielded path inside the with statement. A temporary extracted path may cease to exist after context exit, so do not return that path as a permanent resource location. The acceptance contract records only whether it was a real file while its explicit lifetime was active.

Step 1: Read packaged data through a Traversable, then materialize it only inside an as_file context when a real path is needed. Step 2: Run the accepted baseline, edit the exercise, then check its independent solution cases; preserve caller inputs and the documented exception behavior.

Practice starter: solve before reading the solution

python

import importlib
from importlib.resources import files, as_file
import json
from pathlib import Path
import sys
from zipfile import ZipFile

def inspect_example(minimum):
    archive = Path('owned.zip').resolve()
    package_name = 'owned_resource_demo'
    with ZipFile(archive, 'w') as bundle:
        bundle.writestr(package_name + '/__init__.py', '')
        bundle.writestr(package_name + '/defaults.json', json.dumps({'minimum': minimum}))
    sys.path.insert(0, str(archive))
    importlib.invalidate_caches()
    try:
        package = importlib.import_module(package_name)
        resource = files(package).joinpath('defaults.json')
        settings = json.loads(resource.read_text(encoding='utf-8'))
        return settings
    finally:
        sys.path.remove(str(archive))
        sys.modules.pop(package_name, None)
        sys.path_importer_cache.pop(str(archive), None)

# Task: Read packaged data through a Traversable, then materialize it only inside an as_file context when a real path is needed.
# Practice contract: Read packaged data through a Traversable, then materialize it only inside an as_file context when a real path is needed.
Reasoning
Read packaged data through a Traversable, then materialize it only inside an as_file context when a real path is needed.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Add an observation showing that as_file materializes the selected resource as a real file during its context. Keep reading the settings through the resource API and retain the returned minimum. The first hint is to enter as_file with the existing Traversable.
  • Hint 2: The second hint is to check the yielded path inside the with statement. A temporary extracted path may cease to exist after context exit, so do not return that path as a permanent resource location. The acceptance contract records only whether it was a real file while its explicit lifetime was active.

Solution, tests and trade-offs

The solution uses as_file only for the operation that needs a concrete path and returns a boolean observation from inside that lifetime.

The solution uses as_file only for the operation that needs a concrete path and returns a boolean observation from inside that lifetime. The ordinary read still uses read_text on the Traversable, which avoids unnecessary extraction for a simple text consumer.

The three minimum values prove that the returned data comes from each owned archive rather than a cached constant. Real packages must also include resources in their built distribution; an API for reading packaged data cannot recover a file that was never shipped. The following wheel lesson verifies that separate publication boundary.

Practice solution and acceptance checks

python

import importlib
from importlib.resources import files, as_file
import json
from pathlib import Path
import sys
from zipfile import ZipFile

def inspect_example(minimum):
    archive = Path('owned.zip').resolve()
    package_name = 'owned_resource_demo'
    with ZipFile(archive, 'w') as bundle:
        bundle.writestr(package_name + '/__init__.py', '')
        bundle.writestr(package_name + '/defaults.json', json.dumps({'minimum': minimum}))
    sys.path.insert(0, str(archive))
    importlib.invalidate_caches()
    try:
        package = importlib.import_module(package_name)
        resource = files(package).joinpath('defaults.json')
        settings = json.loads(resource.read_text(encoding='utf-8'))
        with as_file(resource) as path:
            settings['materialized'] = path.is_file()
        return settings
    finally:
        sys.path.remove(str(archive))
        sys.modules.pop(package_name, None)
        sys.path_importer_cache.pop(str(archive), None)
Reasoning
The solution uses as_file only for the operation that needs a concrete path and returns a boolean observation from inside that lifetime. The ordinary read still uses read_text on the Traversable, which avoids unnecessary extraction for a simple text consumer.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Explain why __file__ is insufficient evidence of an ordinary resource directory.

Explain why __file__ is insufficient evidence of an ordinary resource directory. Then describe when read_text is enough and when as_file is appropriate. Include the materialized path's lifetime in the answer instead of assuming every resource path remains valid forever.

For a stretch, make an owned consumer that requires a filename and call it only inside as_file. Compare this with a consumer that accepts bytes directly. Check the built wheel's contents separately, so success in the source tree cannot hide a missing resource in the package delivered to users.

Optional stretch — after the core task works: For a stretch, make an owned consumer that requires a filename and call it only inside as_file. Compare this with a consumer that accepts bytes directly. Check the built wheel's contents separately, so success in the source tree cannot hide a missing resource in the package delivered to users.

Computer practice: https://applydjinn.com/typorithm/python-engineering-lab-20261107.zip. Use an isolated Python 3.13 environment, install requirements.txt, then run verify.py. Edit exercises/pye-resources.py and run --practice pye-resources. Fixture files are created in fresh temporary directories for every case. Packaging checks build real local artifacts, install the exact wheel without an index and run outside the source tree. No package is published to PyPI. The app includes static examples, prepared results and questions; it does not run Python or provide an editor.

Read package data without assuming every imported module lives in an ordinary directory.