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
| Extension | Loader | Behavior |
|---|---|---|
.py | python | Stored as-is; optionally minified with --minify |
.json | json | Parsed at bundle time; exported as a Python dict |
.toml | toml | Parsed at bundle time; exported as a Python dict |
.txt | text | Stored as a UTF-8 string, exported as content: str |
.bin | binary | Stored 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.dataThe 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:
- Check if any plugin transform hook handles the file extension.
- If no plugin claims the extension, fall back to the built-in loader table.
- 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.