t template to ensure the LLM returns structured data.
from pydantic import BaseModel, Field
from typing import List
class SecurityFinding(BaseModel):
severity: str = Field(description="Critical, High, Medium, or Low")
description: str = Field(description="Brief explanation of the vulnerability")
remediation: str = Field(description="Suggested fix for the issue")
class CodeAnalysisResult(BaseModel):
summary: str
findings: List[SecurityFinding]
risk_score: int = Field(ge=0, le=100)
2. Create the Prompt Template
Prompts are stored as Markdown files with YAML frontmatter. The frontmatter declares model parameters and links the response schema. The body uses Jinja2 syntax for dynamic variables.
File: prompts/security_audit.md
---
model: gpt-4o
temperature: 0.1
max_tokens: 1500
response_schema: CodeAnalysisResult
---
You are a senior security engineer. Analyze the provided code snippet for vulnerabilities.
**Author:** {{ author_handle }}
**Context:** {{ project_context }}
**Code:**
```{{ language }}
{{ source_code }}
Return your analysis strictly adhering to the defined schema. Focus on injection flaws, insecure dependencies, and data exposure.
#### 3. Initialize the Engine
The `DynaPrompt` engine loads configurations from specified directories. It supports lazy loading, meaning templates are parsed only when accessed, reducing startup latency.
```python
from dynaprompt import DynaPrompt
# Initialize with the directory containing prompt files
prompt_engine = DynaPrompt(
settings_files=["./prompts/"],
env="development"
)
4. Render and Execute
Rendering injects variables into the template and returns an object containing the rendered text, configuration, and schema. This object can be passed directly to an LLM client.
# Render the template with runtime variables
rendered = prompt_engine.security_audit.render(
author_handle="dev_alice",
project_context="Authentication module",
language="python",
source_code="def verify(token): return token == 'admin'"
)
# Access configuration and schema
model_name = rendered.config["model"]
temperature = rendered.config["temperature"]
schema_class = rendered.response_schema
# Execute via LLM client (example using OpenAI SDK)
response = client.chat.completions.create(
model=model_name,
temperature=temperature,
messages=[{"role": "user", "content": rendered.text}],
response_format={"type": "json_object"} # Schema enforcement handled by client
)
# Parse structured output
result = schema_class.model_validate_json(response.choices[0].message.content)
5. Environment Layering
Configuration files can define environment-specific overrides. This allows switching model parameters or behavior without code changes.
File: prompts/config.toml
[default.security_audit]
model = "gpt-3.5-turbo"
temperature = 0.7
[production.security_audit]
model = "gpt-4o"
temperature = 0.1
max_tokens = 2000
Usage with environment switching:
# Default environment loads default settings
print(prompt_engine.security_audit.config["model"]) # Output: gpt-3.5-turbo
# Context manager switches to production settings temporarily
with prompt_engine.using_env("production"):
print(prompt_engine.security_audit.config["model"]) # Output: gpt-4o
6. Validation and Hooks
Validators and hooks provide guardrails. A validator can block rendering if constraints are violated, while hooks can inject dynamic context.
from dynaprompt.validator import PromptValidator
class TokenBudgetValidator(PromptValidator):
def validate(self, node, rendered) -> None:
# Simple word count approximation for budget check
word_count = len(rendered.text.split())
if word_count > 3000:
raise ValueError(
f"Prompt '{node.name}' exceeds token budget. "
f"Current words: {word_count}"
)
# Register validator during initialization
prompt_engine = DynaPrompt(
settings_files=["./prompts/"],
validators=[TokenBudgetValidator()]
)
# Add a hook to inject request metadata
def inject_request_id(node, kwargs):
kwargs["request_id"] = kwargs.get("request_id", "unknown")
return kwargs
prompt_engine.add_hook("before_render", "request_tracker", inject_request_id)
Pitfall Guide
Even with a robust library, implementation errors can undermine prompt management. The following pitfalls highlight common mistakes and their remedies based on production experience.
-
Schema-Template Desynchronization
- Explanation: Updating the Pydantic schema without updating the prompt instructions can lead to the LLM generating fields that no longer exist or missing required fields.
- Fix: Use the
response_schema frontmatter field to explicitly link the schema. Implement CI checks that verify the schema class exists and is importable when the prompt file is modified.
-
Token Budget Overruns
- Explanation: Dynamic variables like
source_code can inject large amounts of text, exceeding context windows or incurring excessive costs.
- Fix: Implement a
PromptValidator that estimates token usage based on rendered text length. Reject renders that exceed defined thresholds and log warnings for monitoring.
-
Environment Leakage
- Explanation: Accidentally using production model settings in development environments can increase costs and slow down iteration cycles.
- Fix: Always specify the
env parameter during engine initialization. Use the using_env context manager for temporary switches and ensure default configurations are safe for development (e.g., cheaper models, higher temperature).
-
Secret Injection in Templates
- Explanation: Storing API keys or sensitive data directly in prompt files or frontmatter poses a security risk, especially if files are committed to version control.
- Fix: Never store secrets in prompt files. Use hooks to inject sensitive data from secure runtime sources (e.g., environment variables, secret managers) immediately before rendering.
-
Over-Abstraction for Simple Prompts
- Explanation: Creating separate files and schemas for one-off, trivial prompts adds unnecessary complexity and file clutter.
- Fix: Reserve DynaPrompt for reusable, critical, or multi-environment prompts. Inline strings are acceptable for throwaway scripts or internal debugging tools where versioning is not required.
-
Ignoring Response Format Enforcement
- Explanation: Defining a schema in the prompt but failing to enforce
response_format in the LLM API call can result in unstructured text, breaking the parsing logic.
- Fix: Always configure the LLM client to use structured output modes (e.g., JSON mode or function calling) that align with the defined schema. Verify the client supports the specific model's structured output capabilities.
-
Validator Performance Bottlenecks
- Explanation: Heavy validation logic, such as complex regex or external API calls within a validator, can introduce latency to every prompt render.
- Fix: Keep validators lightweight. Use efficient token estimators and avoid blocking operations. If validation requires external checks, consider asynchronous validation or pre-render checks outside the critical path.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Rapid Prototype | Inline Strings | Minimal setup; fast iteration for proof-of-concept. | Low dev cost; high risk of tech debt. |
| Multi-Environment App | DynaPrompt | Ensures consistency across dev/staging/prod; easy parameter tuning. | Low infra cost; moderate setup time. |
| Complex Agent Orchestration | LangChain / LlamaIndex | Provides advanced tools for memory, chains, and agent loops. | High complexity; steeper learning curve. |
| Strict Schema Enforcement | DynaPrompt + Pydantic | Native schema linking reduces parsing errors and drift. | Low runtime cost; improves reliability. |
| High-Volume Production | DynaPrompt + Caching | Lazy loading and config separation optimize resource usage. | Low latency; scalable architecture. |
Configuration Template
Use this TOML template to define environment-specific overrides for your prompts. Place this file in your prompts directory and reference it in the settings_files argument.
# Base configuration for all environments
[default.code_review]
model = "gpt-3.5-turbo"
temperature = 0.7
max_tokens = 1000
response_schema = ReviewResult
# Production overrides
[production.code_review]
model = "gpt-4o"
temperature = 0.1
max_tokens = 2000
# Inherits schema from default
# Staging overrides for testing new models
[staging.code_review]
model = "gpt-4o-mini"
temperature = 0.3
max_tokens = 1500
Quick Start Guide
- Install: Execute
pip install dynaprompt in your project environment.
- Create Template: Add a file
prompts/greeting.md with YAML frontmatter and Jinja2 content.
- Initialize: In your Python script, run
engine = DynaPrompt(settings_files=["prompts/"]).
- Render: Call
result = engine.greeting.render(name="World") to generate the prompt text and config.
- Execute: Pass
result.text and result.config to your LLM client to generate the response.
DynaPrompt provides a structured, scalable approach to prompt management that aligns with software engineering best practices. By externalizing prompts, enforcing schemas, and enabling environment-aware configuration, teams can build LLM applications that are robust, maintainable, and production-ready. The library is MIT licensed and available via PyPI.