Prompt Library in Python: Templates, Storage, and Tests
How to build a Python prompt library using typed templates, variable substitution, and output tests. Keep approved prompt decisions and lessons in fenbs alongside the code.
Updated 4 min read
A Python prompt library stores your AI prompts as typed templates with named variables, tests that verify inputs before a call goes out, and a structured record of what each prompt is meant to do. Keeping prompts in code makes them versionable and testable. A tool like fenbs complements the code layer by keeping the approved decisions and the lessons each revision taught, readable by any connected AI, not just the Python process that runs the library.
The steps below build a small library from scratch. The goal is a pattern you can extend, not a framework. Each section adds one layer: template definition, storage, input validation, and output testing.
A basic template structure
Start with a dataclass that holds the prompt text, its required variable names, and a short description of what it is for. Python's string.Template class provides safe substitution with $variable placeholders and handles missing variables gracefully without raising unexpected errors by default.
from dataclasses import dataclass, field
from string import Template
@dataclass
class PromptTemplate:
name: str
description: str
template: str
required_vars: list[str] = field(default_factory=list)
def render(self, **kwargs: str) -> str:
missing = [v for v in self.required_vars if v not in kwargs]
if missing:
raise ValueError(f"Missing required variables: {missing}")
return Template(self.template).safe_substitute(**kwargs)
def variables(self) -> list[str]:
"""Return all $variable names found in the template text."""
import re
return re.findall(r'\$([A-Za-z_][A-Za-z0-9_]*)', self.template)The safe_substitute method leaves unknown $variable references unchanged rather than raising an error. If you want strict checking, use substitute instead and handle the KeyError it raises when a variable is not supplied. Use safe_substitute when templates may evolve and you want backward compatibility; use substitute when you need to guarantee all variables are filled.
Storing and loading templates
For a small library, a module-level dictionary is the simplest storage. For a library that needs to be edited without redeploying code, a JSON file works well. Both approaches are shown below.
from prompt_template import PromptTemplate
LIBRARY: dict[str, PromptTemplate] = {
"summarize": PromptTemplate(
name="Summarize",
description="Summarize content for a specific audience.",
template="Summarize the following for $audience in $max_words words or fewer:\n\n$content",
required_vars=["audience", "max_words", "content"],
),
"review": PromptTemplate(
name="Review",
description="Review a document against stated criteria.",
template=(
"Review the following $document_type against these criteria: $criteria\n\n"
"$content\n\n"
"For each criterion, say whether the document meets it and why."
),
required_vars=["document_type", "criteria", "content"],
),
"gap_finder": PromptTemplate(
name="Gap finder",
description="Find unanswered questions in a document.",
template=(
"Read the following document.\n"
"List $count questions most likely to come from a reviewer who has not seen the source material.\n"
"For each, say whether the document answers it, answers it partially, or leaves it open.\n\n"
"$content"
),
required_vars=["count", "content"],
),
}
def get(name: str) -> PromptTemplate:
if name not in LIBRARY:
raise KeyError(f"No prompt named '{name}'. Available: {list(LIBRARY)}")
return LIBRARY[name]Testing prompt inputs
Input tests confirm that the render method raises the right errors and produces the expected structure. These tests run without making any API calls, which makes them fast and cheap to run on every commit.
import pytest
from prompt_store import get
def test_summarize_raises_on_missing_vars():
t = get("summarize")
with pytest.raises(ValueError, match="Missing required variables"):
t.render(audience="a lawyer") # missing max_words and content
def test_summarize_renders_with_all_vars():
t = get("summarize")
result = t.render(audience="a lawyer", max_words="100", content="Sample text here.")
assert "lawyer" in result
assert "100" in result
assert "Sample text here." in result
def test_get_raises_for_unknown_name():
with pytest.raises(KeyError):
get("this_does_not_exist")
def test_gap_finder_contains_count_and_content():
t = get("gap_finder")
result = t.render(count="5", content="A short brief.")
assert "5" in result
assert "A short brief." in resultThese tests cover the mechanics of the library, not the quality of the AI's response. They verify that the right text reaches the model, that missing inputs are caught early, and that the library behaves consistently as it grows. That is the appropriate job for unit tests here.
Testing prompt outputs
Output tests call the AI and check that the response meets structural requirements: sections that should appear, claims that should not appear, and format rules. These tests cost money to run and depend on the model's behavior, so run them less frequently than input tests.
- Check for required sections. If a prompt should return a numbered list, assert that the response contains at least one numbered item.
- Check that variable content appears. If you passed "lawyer" as the audience, confirm the response is pitched at a non-technical reader.
- Check for hallucination markers. If the prompt said "use only the content above," assert that phrases from your test content appear in the output.
- Store a sample output alongside the test. When a model update changes behavior, the stored sample shows what the output used to look like.
When to update a template
Update a template when an output test fails consistently, when the task it serves changes, or when a team member finds a better instruction through trial and error. Keep a brief comment in the code noting what changed and why.
The code comment captures the technical change. The reasoning behind it, the problem it was solving, and whether it actually worked are harder to express in a comment and easy to lose. Recording those as lessons in fenbs keeps them beside the project context, readable by any AI working on the same codebase.
A related question: when should you retire a template rather than update it? Retire it when the task it was written for no longer exists, when the new version is incompatible with the old signature, or when the old version caused a problem you want to document rather than overwrite. Mark retired templates clearly rather than deleting them. The record of what did not work is often as useful as the record of what does.
Record what each prompt revision taught the team
fenbs keeps Lessons learned alongside your project tasks and decisions. When a prompt update solves a problem, add a lesson: what the old version did wrong, what changed, and what to check if the problem returns. Any connected AI reads lessons before similar work. Create a free account to build that record alongside your Python library.
Create a free accountFree for one person · No credit card · Works with Claude, ChatGPT, Cursor and more
How to do this with fenbs
The Python library handles code-level concerns: structure, validation, and testing. fenbs handles the team-level concerns: which prompt is currently approved, why it is shaped the way it is, and what earlier versions taught. Together they cover the full lifecycle of a prompt, from the very first commit to the lesson that shapes the next revision.
- Create a free account. Add a project for the application that uses the prompt library.
- Go to Skills and prompts in your project. Add each template as an entry with its name, the current approved text, and the decisions behind any non-obvious instructions.
- Add a Lesson each time a template revision solves a real problem. Note what failed, what changed, and what the output check confirmed. The next developer who maintains the library can read that history before making changes.
- Connect your AI apps at Integrations. A connected assistant can retrieve the current approved prompt text and the lessons attached to it, so code reviews and prompt reviews work from the same record.