Skip to main content

Overview

The resources system provides unified access to static files (templates, icons, configs, etc.) that work identically in development and PyInstaller-bundled executables. Using importlib.resources, it abstracts away environment-specific path resolution.
Resources are static files bundled with your Python package. They’re accessible whether your code is running from source files or a PyInstaller executable.

Why Resources?

When distributing Python applications as executables, traditional file access breaks:
# ❌ Breaks in PyInstaller executables
with open("templates/config.json") as f:
    config = json.load(f)

# ❌ Also breaks - relies on __file__ which may not exist
module_dir = Path(__file__).parent
template_path = module_dir / "templates" / "email.html"
With resources:
# ✓ Works in development AND executables
from pyrig.src.resource import resource_path
import myapp.templates

template_path = resource_path("email.html", myapp.templates)
with open(template_path) as f:
    template = f.read()

The resource_path Function

Pyrig provides a single function for resource access:
src/resource.py
def resource_path(name: str, package: ModuleType) -> Path:
    """Get filesystem path to a resource file within a package.

    Provides cross-platform, environment-agnostic access to static resources
    bundled with Python packages.

    Args:
        name: Resource filename (e.g., "config.json", "icon.png").
            Can include subdirectory paths relative to the package
            (e.g., "templates/email.html").
        package: Package module object containing the resource.
            Import the package's __init__.py module and pass it directly.

    Returns:
        Absolute path to the resource file.

    Example:
        >>> from pyrig import resources
        >>> path = resource_path("GITIGNORE", resources)
        >>> content = path.read_text()
    """
    resource_path = files(package) / name
    with as_file(resource_path) as path:
        return path

Implementation Details

Under the hood, resource_path uses importlib.resources:
from importlib.resources import as_file, files

resource_path = files(package) / name
with as_file(resource_path) as path:
    return path
  • files(package) - Returns a traversable resource in the package
  • as_file() - Ensures the resource is accessible as a filesystem path
  • Development - Returns the actual file path
  • PyInstaller - Extracts to a temporary directory and returns that path

How to Use Resources

Step 1: Organize Resources

Create a package to hold your resources:
myapp/
  resources/
    __init__.py          # Empty or with docstring
    icon.png
    config.json
    templates/
      __init__.py
      email.html
      report.md

Step 2: Import the Package

import myapp.resources
import myapp.resources.templates

Step 3: Access Resources

from pyrig.src.resource import resource_path
import myapp.resources
import myapp.resources.templates

# Get resource paths
icon_path = resource_path("icon.png", myapp.resources)
config_path = resource_path("config.json", myapp.resources)
template_path = resource_path("email.html", myapp.resources.templates)

# Use the paths
with open(config_path) as f:
    config = json.load(f)

with open(template_path) as f:
    template = f.read()

image = Image.open(icon_path)
The returned path is valid immediately and remains valid after the function returns (for file-based packages, which is always the case for pyrig projects).

Pyrig’s Built-in Resources

Pyrig includes resource files used during project initialization:
from pyrig.src.resource import resource_path
from pyrig import resources

# Pyrig's resources
gitignore_template = resource_path("GITIGNORE", resources)
license_template = resource_path("LICENSE.md", resources)

# Read the content
content = gitignore_template.read_text()

Example: GitignoreConfigFile

Pyrig uses resources to populate config files:
rig/configs/git/gitignore.py
from pyrig import resources
from pyrig.src.resource import resource_path
from pyrig.rig.configs.base.string_ import StringConfigFile

class GitignoreConfigFile(StringConfigFile):
    """Manages .gitignore file."""

    def parent_path(self) -> Path:
        return Path(".")

    def filename(self) -> str:
        return ".gitignore"

    def extension(self) -> str:
        return ""  # No extension

    def lines(self) -> list[str]:
        # Load from bundled resource
        resource = resource_path("GITIGNORE", resources)
        return resource.read_text().splitlines()
This works whether pyrig is installed from source or as a bundled executable.

PyInstaller Integration

Data Files Configuration

When building with PyInstaller, specify data files in your spec file:
myapp.spec
a = Analysis(
    ['myapp/main.py'],
    datas=[
        ('myapp/resources', 'myapp/resources'),  # Include resources directory
    ],
    ...
)
Or use --add-data flag:
pyinstaller myapp/main.py \
    --add-data "myapp/resources:myapp/resources"

How It Works

  1. Development: resource_path returns the actual file path in your source tree
  2. PyInstaller Build: Resources are packaged into the executable
  3. PyInstaller Runtime: Resources are extracted to a temporary directory, and resource_path returns the temp path
The temporary directory is cleaned up when the application exits. If you need persistent files, copy them to a permanent location.

Common Patterns

Pattern 1: Configuration Templates

from pyrig.src.resource import resource_path
import myapp.resources
import json

def load_default_config() -> dict:
    """Load default configuration from resource."""
    config_path = resource_path("default_config.json", myapp.resources)
    with open(config_path) as f:
        return json.load(f)

Pattern 2: Email Templates

from pyrig.src.resource import resource_path
import myapp.resources.templates
from string import Template

def render_email(name: str, message: str) -> str:
    """Render email from template."""
    template_path = resource_path("email.html", myapp.resources.templates)
    template_content = template_path.read_text()

    template = Template(template_content)
    return template.substitute(name=name, message=message)

Pattern 3: Icon/Image Assets

from pyrig.src.resource import resource_path
import myapp.resources.images
from PIL import Image

def load_app_icon() -> Image:
    """Load application icon."""
    icon_path = resource_path("icon.png", myapp.resources.images)
    return Image.open(icon_path)

Pattern 4: Data Files

from pyrig.src.resource import resource_path
import myapp.resources.data
import csv

def load_lookup_table() -> list[dict]:
    """Load lookup table from CSV resource."""
    csv_path = resource_path("lookup.csv", myapp.resources.data)
    with open(csv_path) as f:
        return list(csv.DictReader(f))

Best Practices

Create subdirectories for different resource types:
myapp/
  resources/
    __init__.py
    images/
      __init__.py
      icon.png
      logo.svg
    templates/
      __init__.py
      email.html
      report.md
    data/
      __init__.py
      config.json
      lookup.csv
Pass the imported module object, not strings:
# ✓ Correct
import myapp.resources
path = resource_path("icon.png", myapp.resources)

# ✗ Wrong
path = resource_path("icon.png", "myapp.resources")  # TypeError!
Keep resource paths relative to the package:
# ✓ Good - relative to package
path = resource_path("templates/email.html", myapp.resources)

# ✗ Avoid - use subpackages instead
path = resource_path("../other_package/file.txt", myapp.resources)
If you access the same resource repeatedly, cache it:
from functools import cache
from pyrig.src.resource import resource_path
import myapp.resources

@cache
def get_large_lookup_table() -> dict:
    """Load and cache large resource."""
    path = resource_path("large_data.json", myapp.resources)
    with open(path) as f:
        return json.load(f)
Ensure every resource directory is a Python package:
myapp/
  resources/
    __init__.py          # Required!
    templates/
      __init__.py        # Required!
      email.html
Without __init__.py, the directory won’t be treated as a package.

Troubleshooting

FileNotFoundError

# Error: FileNotFoundError: resource not found
path = resource_path("missing.json", myapp.resources)
Solutions:
  • Verify the file exists in the package directory
  • Check the spelling of the filename
  • Ensure the package has an __init__.py file
  • For PyInstaller, verify data files are included in the spec

TypeError: package is not a module

# Error: TypeError
path = resource_path("icon.png", "myapp.resources")  # String!
Solution: Pass the imported module object:
import myapp.resources
path = resource_path("icon.png", myapp.resources)  # Module object

Resources Not Found in PyInstaller Executable

Check your spec file includes data files:
a = Analysis(
    ['myapp/main.py'],
    datas=[
        ('myapp/resources', 'myapp/resources'),  # Add this
    ],
)
Or use —add-data flag:
pyinstaller myapp/main.py --add-data "myapp/resources:myapp/resources"

Comparison with Alternatives

vs __file__ Attribute

# ❌ Breaks in PyInstaller (no __file__ in frozen executables)
module_dir = Path(__file__).parent
config_path = module_dir / "config.json"

# ✓ Works everywhere
from pyrig.src.resource import resource_path
import myapp.resources
config_path = resource_path("config.json", myapp.resources)

vs Hardcoded Paths

# ❌ Breaks when installed or moved
config_path = Path("/home/user/myapp/config.json")

# ✓ Works regardless of installation location
config_path = resource_path("config.json", myapp.resources)

vs pkg_resources (deprecated)

# ❌ Deprecated (setuptools)
import pkg_resources
path = pkg_resources.resource_filename("myapp.resources", "icon.png")

# ✓ Modern (importlib.resources)
from pyrig.src.resource import resource_path
import myapp.resources
path = resource_path("icon.png", myapp.resources)

Complete Example

Here’s a complete example of using resources in a project:
myapp/resources/__init__.py
"""Static resources for myapp."""
myapp/resources/config.json
{
  "database": {
    "host": "localhost",
    "port": 5432
  }
}
myapp/src/config.py
from pathlib import Path
import json
from pyrig.src.resource import resource_path
import myapp.resources

def load_default_config() -> dict:
    """Load default configuration from resources."""
    config_path = resource_path("config.json", myapp.resources)
    with open(config_path) as f:
        return json.load(f)

def load_user_config() -> dict:
    """Load user configuration, falling back to default."""
    user_config_path = Path.home() / ".myapp" / "config.json"

    if user_config_path.exists():
        with open(user_config_path) as f:
            return json.load(f)

    # Fall back to default from resources
    return load_default_config()
myapp/main.py
from myapp.src.config import load_user_config

def main() -> None:
    config = load_user_config()
    print(f"Database: {config['database']['host']}:{config['database']['port']}")

if __name__ == "__main__":
    main()

Configuration System

How config files use resources for templates

Builders

How PyInstaller builders package resources

Resources Documentation

Full documentation on resource management

PyInstaller Builder

Creating executables with bundled resources