Skip to content

API Reference

This page contains the automatically generated documentation for the DynaPrompt library.

dynaprompt.DynaPrompt(settings_files, environments=True, env=None, validators=None, file_prefix=None, variables=None, auto_render=True, auto_export=False, structure_mode=True)

Lazy-loading prompt configuration manager. Inspired by Dynaconf's LazySettings — zero I/O at instantiation.

Source code in dynaprompt/core.py
def __init__(
    self,
    settings_files: list[Any],
    environments: bool = True,
    env: str | None = None,
    validators: list[PromptValidator] | None = None,
    file_prefix: str | None = None,
    variables: list[Any] | None = None,
    auto_render: bool = True,
    auto_export: str | bool = False,
    structure_mode: bool = True,
):
    self._settings_files = settings_files
    self._environments = environments
    self._env = env or os.environ.get("ENV_FOR_DYNAPROMPT", "development")
    self._file_prefix = file_prefix
    self._auto_render = auto_render
    self._auto_export = auto_export
    self._structure_mode = structure_mode
    self._validators = ValidatorList()
    if validators:
        self._validators.extend(validators)
    self._hooks: dict[str, list[Hook]] = {}
    self.schemas: dict[str, Any] = {}
    self._variables = variables
    self._wrapped: _PromptSettings | None = None

    # Capture caller's file to avoid self-loading/infinite loops
    import inspect

    try:
        # We skip frames that are inside dynaprompt
        stack = inspect.stack()
        self._caller_file = None
        for frame in stack:
            if "dynaprompt" not in frame.filename:
                self._caller_file = pathlib.Path(frame.filename).resolve()
                break
    except Exception:
        self._caller_file = None

prompts property

Returns all loaded prompts as a dictionary mapping name to PromptNode.

get(name)

Source code in dynaprompt/core.py
def get(self, name: str) -> PromptNode:
    return self.__getattr__(name)

using_env(env)

Source code in dynaprompt/core.py
@contextmanager
def using_env(self, env: str):
    if self._wrapped is None:
        self._setup()
    old_env = self._wrapped._layer.current_env
    self._wrapped.switch_env(env)
    self._env = env
    try:
        yield self
    finally:
        self._wrapped.switch_env(old_env)
        self._env = old_env

inspect(name=None)

Source code in dynaprompt/core.py
def inspect(self, name: str | None = None) -> dict:
    if self._wrapped is None:
        self._setup()
    return self._wrapped.get_history(name)

debug_trace(name)

Prints a visual hierarchy of overrides for a specific prompt. Helps trace exactly which environment or file provided which values.

Source code in dynaprompt/core.py
def debug_trace(self, name: str) -> None:
    """
    Prints a visual hierarchy of overrides for a specific prompt.
    Helps trace exactly which environment or file provided which values.
    """
    history = self.inspect(name)
    if not history:
        print(f"No trace history found for prompt '{name}'.")
        return

    print(f"\n🔍 Debug Trace: '{name}'")
    print("=" * 50)

    for i, entry in enumerate(history):
        env = entry.get("env", "default").upper()
        loader = entry.get("loader", "unknown")
        identifier = entry.get("identifier", "")
        value = entry.get("value", {})

        print(f"📦 [{env}] Loaded via {loader}")
        print(f"   Source: {identifier}")

        if not value:
            print("   └─ (Empty)\n")
            continue

        # Format the dictionary nicely
        import json

        val_str = json.dumps(value, indent=2, default=str)
        lines = val_str.splitlines()
        for j, line in enumerate(lines):
            prefix = "   └─ " if j == len(lines) - 1 else "   ├─ "
            if j == 0 or j == len(lines) - 1:
                if line.strip() in ("{", "}"):
                    continue  # Skip bare braces for cleaner look
            print(f"{prefix}{line}")
        print()

reload()

Source code in dynaprompt/core.py
def reload(self) -> None:
    self._wrapped = None

export_to_toml(filepath='pyprompts.toml')

Export the loaded prompt structure to a TOML file.

Source code in dynaprompt/core.py
def export_to_toml(self, filepath: str = "pyprompts.toml") -> None:
    """Export the loaded prompt structure to a TOML file."""
    from .utils import export_to_toml

    export_to_toml(self, filepath)

add_validator(*validators)

Source code in dynaprompt/core.py
def add_validator(self, *validators: PromptValidator) -> None:
    self._validators.extend(validators)
    if self._wrapped:
        self._wrapped._validators = self._validators

add_hook(event, name_or_hook, hook=None)

Register hooks. Safe to call before or after first access.

Source code in dynaprompt/core.py
def add_hook(self, event: str, name_or_hook: Any, hook: Hook | None = None) -> None:
    """Register hooks. Safe to call before or after first access."""
    if hook is None:
        hook = name_or_hook
        key = event
    else:
        name = name_or_hook
        key = f"{event}_{name}"
    self._hooks.setdefault(key, []).append(hook)
    if self._wrapped:
        self._wrapped._hooks = self._hooks

keys()

Source code in dynaprompt/core.py
def keys(self) -> list[str]:
    if self._wrapped is None:
        self._setup()
    return list(self._wrapped._store.keys())

dynaprompt.nodes.PromptNode(name, text, metadata=None, response_schema=None, parent_template=None, history=None, variables=None, validators=None, hooks=None, current_env='default', auto_render=False)

Represents a single parsed prompt. Supports fluent config overrides and Jinja2 rendering with secret injection.

Source code in dynaprompt/nodes.py
def __init__(
    self,
    name: str,
    text: str,
    metadata: dict[str, Any] = None,
    response_schema: type[BaseModel] | None = None,
    parent_template: str | None = None,
    history: list[tuple] = None,
    variables: dict[str, Any] = None,
    validators: ValidatorList = None,
    hooks: dict[str, list] = None,
    current_env: str = "default",
    auto_render: bool = False,
):
    self.name = name
    self.text = text
    self.raw_template = text
    self.metadata = metadata or {}
    self.response_schema = response_schema
    self._parent_template = parent_template
    self._history = history or []
    self.variables = VariableDict(variables or {})
    self._validators = validators or ValidatorList()
    self._hooks = hooks or {}
    self._current_env = current_env
    self._auto_render = auto_render
    self._overrides: dict[str, Any] = {}
    self.bound_kwargs: dict[str, Any] = {}

    if self.response_schema is None:
        try:
            from pydantic import BaseModel

            # Auto-detect Pydantic schemas referenced in the template text
            pydantic_models = [
                v
                for k, v in self.variables.items()
                if isinstance(v, type)
                and issubclass(v, BaseModel)
                and k in self.raw_template
            ]
            if len(pydantic_models) == 1:
                self.response_schema = pydantic_models[0]
        except ImportError:
            pass

    self._setup_template()

render(*args, **kwargs)

Render the prompt template with the provided variables. Runs validators → Jinja2 → after_render hooks.

Source code in dynaprompt/nodes.py
@hookable
def render(self, *args, **kwargs) -> RenderedPrompt:
    """
    Render the prompt template with the provided variables.
    Runs validators → Jinja2 → after_render hooks.
    """
    for arg in args:
        if isinstance(arg, dict):
            kwargs.update(arg)

    self.bound_kwargs.update(kwargs)

    self._validators.validate(
        self, self.bound_kwargs, current_env=self._current_env
    )

    context = self._build_render_context(self.bound_kwargs)

    try:
        rendered_text = self._compiled_template.render(**context)
    except Exception as exc:
        raise RuntimeError(f"Failed to render prompt '{self.name}': {exc}") from exc

    final_config = {**self.metadata, **self._overrides}
    p_hash = self._compute_hash(rendered_text, final_config)

    return RenderedPrompt(
        text=rendered_text,
        config=final_config,
        response_schema=self.response_schema,
        source_history=self._history,
        prompt_hash=p_hash,
    )

async_render(*args, **kwargs) async

Asynchronously render the prompt template (I/O non-blocking). Runs validators → Jinja2 async → after_render hooks.

Source code in dynaprompt/nodes.py
@async_hookable
async def async_render(self, *args, **kwargs) -> RenderedPrompt:
    """
    Asynchronously render the prompt template (I/O non-blocking).
    Runs validators → Jinja2 async → after_render hooks.
    """
    for arg in args:
        if isinstance(arg, dict):
            kwargs.update(arg)

    self.bound_kwargs.update(kwargs)

    self._validators.validate(
        self, self.bound_kwargs, current_env=self._current_env
    )

    context = self._build_render_context(self.bound_kwargs)

    try:
        rendered_text = await self._compiled_template.render_async(**context)
    except Exception as exc:
        raise RuntimeError(
            f"Failed to async-render prompt '{self.name}': {exc}"
        ) from exc

    final_config = {**self.metadata, **self._overrides}
    p_hash = self._compute_hash(rendered_text, final_config)

    return RenderedPrompt(
        text=rendered_text,
        config=final_config,
        response_schema=self.response_schema,
        source_history=self._history,
        prompt_hash=p_hash,
    )

rerender(**kwargs)

Alias for render(). Useful for explicitly updating a subset of previously provided variables while retaining the rest.

Source code in dynaprompt/nodes.py
def rerender(self, **kwargs) -> RenderedPrompt:
    """
    Alias for render(). Useful for explicitly updating a subset of previously
    provided variables while retaining the rest.
    """
    return self.render(**kwargs)

async_rerender(**kwargs) async

Async alias for rerender().

Source code in dynaprompt/nodes.py
async def async_rerender(self, **kwargs) -> RenderedPrompt:
    """Async alias for rerender()."""
    return await self.async_render(**kwargs)
    """
    Render and (in the future) call an LLM provider directly.
    """
    raise NotImplementedError(
        "LLM invocation not yet implemented — use render() and call your "
        "LLM client directly."
    )

dynaprompt.nodes.RenderedPrompt(text, config, response_schema=None, source_history=list(), prompt_hash='') dataclass

The final output of PromptNode.render() — fully interpolated text + config.

schema_dict property

Returns the response_schema as a JSON Schema dictionary.

schema_json property

Returns the response_schema as a formatted JSON Schema string.

dynaprompt.validator.PromptValidator(*names, requires=None, max_tokens=None, response_schema=None, condition=None, when=None, env=None, description=None)

Declarative validator attached to a prompt name.

Usage::

PromptValidator('customer_support',
    requires=['user_name', 'issue'],
    max_tokens=4096,
    env='production',
)

# Composition with & / |
PromptValidator('p1', requires=['a']) & PromptValidator('p2', requires=['b'])
Source code in dynaprompt/validator.py
def __init__(
    self,
    *names: str,
    requires: list[str] = None,
    max_tokens: int | None = None,
    response_schema=None,
    condition: Callable | None = None,
    when: PromptValidator | None = None,
    env: str | Sequence[str] | None = None,
    description: str = None,
):
    self.names = names
    self.requires = requires or []
    self.max_tokens = max_tokens
    self.response_schema = response_schema
    self.condition = condition
    self.when = when
    self.description = description
    if isinstance(env, str):
        self.envs = [env]
    elif env:
        self.envs = list(env)
    else:
        self.envs = []

validate(prompt_node, kwargs, current_env='default')

Raise ValidationError if invalid.

Source code in dynaprompt/validator.py
def validate(self, prompt_node, kwargs: dict, current_env: str = "default") -> None:
    """Raise ValidationError if invalid."""
    # Honor `when` condition — skip if when itself fails
    if self.when is not None:
        try:
            self.when.validate(prompt_node, kwargs, current_env)
        except ValidationError:
            return  # condition not met, skip this validator

    # Honor env scope
    if self.envs and current_env not in self.envs:
        return

    # Filter by prompt name scope (empty names = applies to all)
    if self.names and prompt_node.name not in self.names:
        return

    # 1. Required variables check
    for var in self.requires:
        if var not in kwargs:
            raise ValidationError(
                f"Prompt '{prompt_node.name}' requires variable '{var}' "
                f"but it was not provided to .render()."
            )

    # 2. Token estimate guard (rough: 1 word ≈ 1.3 tokens)
    if self.max_tokens is not None:
        word_count = len(prompt_node.text.split())
        estimated_tokens = int(word_count * 1.3)
        if estimated_tokens > self.max_tokens:
            raise ValidationError(
                f"Prompt '{prompt_node.name}' estimated ~{estimated_tokens} tokens "
                f"but max_tokens={self.max_tokens}."
            )

    # 3. Custom callable condition
    if self.condition is not None:
        if not self.condition(prompt_node, kwargs):
            raise ValidationError(
                f"Prompt '{prompt_node.name}' failed custom validation condition."
            )