pydantic-promptmodel
Compile typed Python prompt structures into deterministic Markdown or XML.
pydantic-promptmodel gives prompt code the same properties you expect from other
application code: typed inputs, validation, reusable structure, deterministic output,
and reviewable diffs. Define the meaning once, then choose Markdown or XML without
maintaining parallel templates.
In 20 seconds
uv add pydantic-promptmodel
Or use pip install pydantic-promptmodel.
from pydantic_promptmodel import PromptModel
class ReviewPrompt(PromptModel):
_title: str = "Code Review"
_body: str = "Review the supplied change as a senior engineer."
task: str
checks: tuple[str, ...] = ("Correctness", "Security", "Maintainability")
prompt = ReviewPrompt(task="Return prioritized findings with concrete fixes.")
# Code Review
Review the supplied change as a senior engineer.
**Task:** Return prioritized findings with concrete fixes.
## Checks
- Correctness
- Security
- Maintainability
markdown_prompt = prompt.to_markdown()
xml_prompt = prompt.to_xml()
Both outputs preserve the same field order, hierarchy, values, and repetition. The renderer owns heading levels, labels, list markers, XML names, indentation, and escaping.
The maintenance problem it removes
An interpolated prompt begins simply, but its structure, escaping, validation, and alternate formats quickly become application responsibilities:
prompt_text = f"""# Support Triage
Classify the ticket and propose the next action.
## Priorities
- Protect account access
- Avoid unsupported promises
## Ticket
```text
{ticket_text}
```
"""
Represent the same meaning as typed data instead:
from typing import Annotated
from pydantic_promptmodel import PromptModel, Slot
class TriagePrompt(PromptModel):
_title: str = "Support Triage"
_body: str = "Classify the ticket and propose the next action."
priorities: tuple[str, ...] = (
"Protect account access",
"Avoid unsupported promises",
)
ticket: Annotated[str, Slot()]
prompt_text = TriagePrompt(ticket=ticket_text).to_markdown()
Pydantic validates the input, the renderer safely expands a Markdown fence when the
ticket contains backticks, and .to_xml() remains available without a second prompt
definition.
Who is this for?
Use pydantic-promptmodel when prompts are maintained as application code and you
care about:
- reviewing prompt changes as stable pull-request diffs;
- validating runtime inputs before rendering;
- sharing one prompt structure across model providers or output formats;
- snapshot testing complete prompts;
- composing prompts from existing Pydantic models or dataclasses; or
- making data boundaries and unsupported structures explicit.
It deliberately produces one canonical representation per format. It is not intended for pixel-level control over arbitrary Markdown/XML layouts or extensive template control flow.
What you get
- One typed source of truth. Nested models, collections, schemas, literal examples, and runtime values compose through normal Python types.
- Canonical Markdown and XML. The two renderers lower independently from one shared plan; neither format is converted into the other.
- Deterministic output. Declaration order and stored sequence order are preserved, making snapshots and prompt review useful.
- Pydantic validation. Prompt values are checked before they reach a model client.
- Useful defaults with narrow overrides. Ordinary models need little or no metadata; explicit annotations handle genuine ambiguity such as tables or XML item names.
- No required inheritance. Standalone functions render existing Pydantic models and standard dataclasses.
Render an existing model
You do not need to inherit from PromptModel:
from pydantic import BaseModel
from pydantic_promptmodel import render_markdown, render_xml
class FlightContext(BaseModel):
departure_city: str
arrival_city: str
context = FlightContext(departure_city="Lisbon", arrival_city="Tel Aviv")
markdown_context = render_markdown(context, title="Flight Context")
xml_context = render_xml(context)
Standard dataclass instances use the same standalone renderers. If you own an
existing BaseModel and prefer method syntax, inherit as
class FlightContext(BaseModel, PromptModel).
Use it with Pydantic AI
Rendered prompts are ordinary strings, so they can be passed directly to a model client or agent framework. With Pydantic AI:
from pydantic_ai import Agent
agent = Agent(
"openai:gpt-5.2",
system_prompt=prompt.to_markdown(),
)
result = agent.run_sync("Review src/checkout.py.")
Pydantic AI remains an independent optional dependency; install it separately when using this integration. The rendered XML can be supplied in the same way.
Snapshot-test a prompt
Deterministic rendering makes a checked-in prompt snapshot an exact contract:
from pathlib import Path
def test_code_review_prompt() -> None:
expected = Path("tests/snapshots/code-review.md").read_text()
assert prompt.to_markdown() == expected
A semantic model change produces an ordinary text diff. Accidental changes to headings, order, labels, escaping, or runtime boundaries fail the test.
How it works
PromptModel / Pydantic BaseModel / dataclass
|
reflection and validation
|
shared rendering plan
/ \
canonical Markdown canonical XML
The public plan is inspectable, but normal application code renders model instances directly.
Complete examples
- Travel support models tools, procedures, ordered handling rules, and repeated records.
- Code review isolates a runtime diff and renders a prompt-visible output schema.
- Research brief combines runtime source material with typed research and citation requirements.
Each example is runnable with uv run python examples/<name>.py.
Rich rendering structures
Natural model hierarchy becomes sections and repeated elements. Use explicit local metadata only where the model alone cannot choose the intended presentation.
For example, Table() renders a homogeneous sequence of shallow records as a
Markdown table while XML retains its normal repeated-element sequence:
from typing import Annotated
from pydantic_promptmodel import PromptModel, Table
class Finding(PromptModel):
severity: str
line_number: int
message: str
class ReviewResult(PromptModel):
findings: Annotated[list[Finding], Table()]
# Findings
| Severity | Line Number | Message |
| --- | --- | --- |
| high | 42 | Reject malformed input. |
| low | 8 | Clarify the example. |
Table() supports one-depth records with single-line scalar cells. Optional values
become empty cells, declaration and row order are preserved, and Ordered() adds a
numbered first column. Unsupported table shapes fail with a structural location.
Other narrow overrides include Label, XmlName, ItemName, Inline, Block,
Ordered, LiteralBlock, and Slot.
Runtime data boundaries
Slot() marks validated runtime data. Markdown places its value inside an
escape-aware code fence; XML emits escaped character data. This prevents runtime
content from altering the generated Markdown or XML structure.
The boundary is syntactic, not semantic. A model can still interpret instructions
inside fenced or escaped data, so Slot() is not a prompt-injection defense.
LiteralBlock provides the same opaque rendering behavior for prompt-authored
examples such as JSON, source code, or logs.
Document-level controls
Renderer options apply across the complete model graph:
markdown_fragment = render_markdown(
context,
naming="verbatim",
title="flight_context",
start_level=2,
heading_overflow="bold",
scalar_style="auto",
empty_sequences="marker",
)
xml_prompt = render_xml(
context,
naming="verbatim",
root_name="flight_context",
fallback_item_name="criterion",
)
naming="verbatim" preserves identifiers. Markdown scalar and heading policies
control canonical presentation without changing the model. XML root and fallback
item names resolve document-level ambiguity. Field-local metadata takes precedence.
Design philosophy
The model is the source of truth. The library aims to produce one clear, correct, and useful representation for each format—not reproduce every possible hand-written Markdown or XML layout.
Supports Python 3.11+ and Pydantic 2.
Learn more
Development
make format
make lint
make test
make check
make build
Release maintainers should follow the trusted-publishing guide.
Metadata
Release files for pydantic-promptmodel 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pydantic_promptmodel-0.4.1.tar.gz | 18.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydantic_promptmodel-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.4 kB
Release files / pydantic_promptmodel-0.4.1.tar.gz
| Download URL | pydantic_promptmodel-0.4.1.tar.gz |
|---|---|
| Size | 18.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cf8331fd31dbedf757909610ffebe6fd246f46ba4cec93e1e8ba84720ad8e02a
|
|
BLAKE2b-256 checksum How to use checksums |
fc344e2a419a4962e11f353bcdfa8b7a836ce7be3e667d5fc5deac777c1bbd17
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.
Transparency logRelease files / pydantic_promptmodel-0.4.1-py3-none-any.whl
| Download URL | pydantic_promptmodel-0.4.1-py3-none-any.whl |
|---|---|
| Size | 22.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c063fb67cf68bce88d9ed9fbe541c588203fe314011b3d3b6ea266d1856f1968
|
|
BLAKE2b-256 checksum How to use checksums |
d03100419bd3983b27afc2c93c000b8c16f079f026155d0e2e8c2b0fb5e5407a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.
Transparency logSecurity for pydantic-promptmodel 0.4.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pydantic_promptmodel-0.4.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.