Skip to content

Loaders

A loader tells bunpy build how to handle a specific file extension during bundling. When the static import tracer encounters a file, it selects a loader based on the file extension and uses it to convert the file into something that can be stored in the bundle and imported at runtime.

Built-in loaders

ExtensionLoaderBehavior
.pypythonStored as-is; optionally minified with --minify
.jsonjsonParsed at bundle time; exported as a Python dict
.tomltomlParsed at bundle time; exported as a Python dict
.txttextStored as a UTF-8 string, exported as content: str
.binbinaryStored as raw bytes, exported as data: bytes

The .json and .toml loaders let you import data files the same way you import Python modules:

# data/config.json
# {"host": "localhost", "port": 5432}

from data import config
print(config.data["host"])   # "localhost"

The file is parsed once at bundle time. At runtime, the Python dict is constructed from a literal - no file I/O, no json.loads.

Built-in loader: JSON

# assets/schema.json
# {"version": 1, "tables": ["users", "products"]}
from assets import schema

assert schema.data["version"] == 1
assert "users" in schema.data["tables"]

The emitted Python module looks like:

# generated by json loader
data = {"version": 1, "tables": ["users", "products"]}

No runtime json import, no file open - the data is embedded directly in the bundle.

Built-in loader: TOML

# config/database.toml
[connection]
host = "localhost"
port = 5432
name = "myapp"
from config import database

conn_str = (
    f"postgresql://{database.data['connection']['host']}"
    f":{database.data['connection']['port']}"
    f"/{database.data['connection']['name']}"
)

Built-in loader: text

from assets import email_template

# email_template.txt is embedded as a string
body = email_template.content.format(name="Alice")

The emitted module:

# generated by text loader
content = "Dear {name},\n\nThank you for signing up.\n"

Built-in loader: binary

from assets import logo

# logo.bin is embedded as bytes
image_bytes = logo.data

The emitted module:

# generated by binary loader
data = b"\x89PNG\r\n\x1a\n..."

Suitable for small assets like icons or certificates. For large binary files, prefer loading from disk at runtime.

Registering a custom loader

Custom loaders are registered via the plugin system. A loader is a transform hook scoped to a specific extension:

# myloaders.py
from bunpy.build import Plugin, BuildContext, TransformArgs

plugin = Plugin(name="custom-loaders", version="0.1.0")

def setup(ctx: BuildContext) -> None:
    ctx.on_transform(load_csv)
    ctx.on_transform(load_dotenv)

def load_csv(args: TransformArgs) -> str | None:
    if args.loader != ".csv":
        return None
    import csv, io, json
    reader = csv.DictReader(io.StringIO(args.contents))
    rows = list(reader)
    return f"# generated by csv loader\nrows = {json.dumps(rows, indent=2)}\n"

def load_dotenv(args: TransformArgs) -> str | None:
    if args.loader != ".env":
        return None
    # Parse KEY=VALUE pairs and emit a dict
    pairs = {}
    for line in args.contents.splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        key, _, value = line.partition("=")
        pairs[key.strip()] = value.strip()
    import json
    return f"# generated by dotenv loader\nvars = {json.dumps(pairs, indent=2)}\n"
# pyproject.toml
[tool.bunpy.build]
plugins = ["myloaders:plugin"]

Now .csv and .env files can be imported like any other module:

from data import products   # products.csv

for row in products.rows:
    print(row["name"], row["price"])

Example: CSS-in-Python

Embed a CSS file as a string so it can be injected into HTML at runtime:

# css_loader.py
from bunpy.build import Plugin, BuildContext, TransformArgs

plugin = Plugin(name="css-loader", version="0.1.0")

def setup(ctx: BuildContext) -> None:
    ctx.on_transform(load_css)

def load_css(args: TransformArgs) -> str | None:
    if args.loader != ".css":
        return None
    # Minify whitespace and embed as a string constant
    minified = " ".join(args.contents.split())
    return f"# generated by css-loader\ncss = {repr(minified)}\n"
from styles import main   # main.css

html = f"<style>{main.css}</style><h1>Hello</h1>"

Loader resolution order

When bunpy build encounters a file:

  1. Check if any plugin transform hook handles the file extension.
  2. If no plugin claims the extension, fall back to the built-in loader table.
  3. If no built-in loader matches, the file is skipped (not bundled).

Custom loaders registered via plugins always take precedence over built-in loaders for the same extension. This lets you override the built-in .json loader if you need non-standard behavior (e.g., stripping comments from JSON5 before parsing).

Loader configuration in pyproject.toml

You can map additional extensions to built-in loaders without writing a plugin:

[tool.bunpy.build.loaders]
".yaml" = "text"     # embed YAML files as raw strings
".graphql" = "text"  # embed GraphQL schemas as strings
".png" = "binary"    # embed small images as bytes
".webp" = "binary"

This is the lightest-weight option when you only need a file’s raw content at runtime, not a parsed representation.

Size considerations

Every file processed by a loader is embedded in the bundle. Text and binary loaders are lossless - the file size is included in full. For data files larger than a few hundred KB, consider whether embedding makes sense or whether the file should be loaded from disk at runtime. The .pyz format is a ZIP archive, so large text files benefit from compression automatically.