Skip to content
Prompting Press v0.2

Getting started — Python

Terminal window
pip install prompting-press

The PyPI distribution name is prompting-press; the import name is prompting_press. Python 3.12+ required (the wheel is abi3-py312 — one wheel covers CPython 3.12 and up).

A prompt definition is YAML, JSON, or TOML text — the same shape in any of the three formats. The caller reads the text and hands it to the from_* factories; the library does no file I/O. The assistant definition used throughout this page:

assistant.yaml
name: assistant
role: system
body: "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words."
variables:
company:
type: string
trusted: true
max_words:
type: integer
trusted: true

Step 1 — construct a Prompt (and validate)

Section titled “Step 1 — construct a Prompt (and validate)”

Read the definition file and hand its text to a from_* factory (from_yaml / from_json / from_toml), or build from a shape object with Prompt(...) — either a typed PromptDefinition or a plain dict. The library does no file I/O — the caller reads the file. Every route runs the same validation immediately:

getting-started_python_construct_from_text.py
"""Construct a Prompt from a definition file with a from_* factory (validates immediately)."""
from pathlib import Path
from prompting_press import Prompt
# The caller reads the definition; the library does no file I/O itself.
# Resolve the file next to this program (a real app uses its own path).
_HERE = Path(__file__).parent
assistant = Prompt.from_yaml(
(_HERE / "assistant.yaml").read_text()
) # validates here, or raises
# The same definition in JSON or TOML parses into an identical Prompt:
# assistant = Prompt.from_json((_HERE / "assistant.json").read_text())
# assistant = Prompt.from_toml((_HERE / "assistant.toml").read_text())
assert assistant.name == "assistant"
assert (
assistant.body
== "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words."
)

Construction validates immediately: if the body had referenced {{ industry }} — a variable variables doesn’t declare — construction would have raised PromptRenderError (code undefined_variable), before any render (a malformed document raises LoadError). A template/variable disagreement is caught at construction, never as a silent empty render.

The render values are a caller-owned Pydantic model whose fields match the prompt’s variables (company, max_words). This model is the render-time gate: you hand it to render in Step 3, and its validators (here, max_words must be a positive integer) decide what data is allowed to reach the kernel. Declaring it is just the definition — the enforcement happens at render:

getting-started_python_vars_model.py
"""Declare the typed Vars model — a caller-owned Pydantic model whose validators
run before the kernel is touched."""
from pydantic import BaseModel, field_validator
class AssistantVars(BaseModel):
company: str
max_words: int
@field_validator("max_words")
@classmethod
def _at_least_one(cls, v: int) -> int:
if v < 1:
raise ValueError("max_words must be at least 1")
return v
# The model matches the prompt's `variables` (company, max_words) and its validators run first.
ok = AssistantVars(company="Acme Robotics", max_words=50)
assert ok.company == "Acme Robotics"
assert ok.max_words == 50
# A max_words below 1 is rejected by the field validator.
try:
AssistantVars(company="Acme Robotics", max_words=0)
raise AssertionError("expected a validation error for max_words below 1")
except ValueError as exc:
assert "max_words must be at least 1" in str(exc)

Step 3 — render (the model validates the data), and read the result

Section titled “Step 3 — render (the model validates the data), and read the result”

Hand the Prompt from Step 1 the AssistantVars from Step 2 — pass the class plus a data dict. The model runs first — valid data renders; invalid data (e.g. max_words: 0) raises PromptValidationError and the kernel is never touched. The sample shows both the passing render and the rejection:

getting-started_python_render.py
"""Render, and read the result — hand the Prompt the Vars class plus a data dict;
it validates + renders in one call, returning a RenderResult. The same Vars class
also rejects bad data at render, before the kernel ever sees it."""
import re
from pathlib import Path
from prompting_press import Prompt, PromptValidationError
from pydantic import BaseModel, field_validator
class AssistantVars(BaseModel):
company: str
max_words: int
@field_validator("max_words")
@classmethod
def _at_least_one(cls, v: int) -> int:
if v < 1:
raise ValueError("max_words must be at least 1")
return v
# The caller reads the definition; the library does no file I/O itself.
# Resolve the file next to this program (a real app uses its own path).
_HERE = Path(__file__).parent
assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text())
result = assistant.render(
AssistantVars, data={"company": "Acme Robotics", "max_words": 50}
)
assert (
result.text
== "You are a support assistant for Acme Robotics. Keep your replies under 50 words."
)
assert result.variant == "default" # same arm assistant.body showed in Step 1
assert re.fullmatch(
r"[0-9a-f]{64}", result.template_hash
) # 64-char lowercase-hex SHA-256 of the template
assert re.fullmatch(
r"[0-9a-f]{64}", result.render_hash
) # 64-char lowercase-hex SHA-256 of result.text
assert result.guard is None # no guard requested
# The same AssistantVars validates at render: bad data is rejected before the kernel.
try:
assistant.render(AssistantVars, data={"company": "Acme Robotics", "max_words": 0})
raise AssertionError("expected a validation error for max_words below 1")
except PromptValidationError as exc:
row = exc.errors[0]
assert row.field == "max_words"
assert row.code == "validation"
assert "max_words must be at least 1" in row.message

render(model=None, *, data=None, variant=None, guard=None)data/variant/guard are keyword-only:

  • Pass a Pydantic class + data= dict (as above), or a pre-built instance as model with no data.
  • The class-plus-data form is the one to reach for by default — it is what makes the typed-vars validation above actually run.
  • variant=None selects the default (root body) arm; guard=None opts out of the advisory guard.
getting-started_python_complete_example.py
"""Complete example — construct (validates), then render with typed, Pydantic-validated vars."""
import re
from pathlib import Path
from prompting_press import Prompt
from pydantic import BaseModel, field_validator
class AssistantVars(BaseModel):
company: str
max_words: int
@field_validator("max_words")
@classmethod
def _at_least_one(cls, v: int) -> int:
if v < 1:
raise ValueError("max_words must be at least 1")
return v
# 1. Construct (validates here).
# The caller reads the definition; the library does no file I/O itself.
# Resolve the file next to this program (a real app uses its own path).
_HERE = Path(__file__).parent
assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text())
# 2 + 3. Render with the typed, Pydantic-validated vars.
result = assistant.render(
AssistantVars, data={"company": "Acme Robotics", "max_words": 50}
)
print(
result.text
) # You are a support assistant for Acme Robotics. Keep your replies under 50 words.
print(result.template_hash) # 64-char hex
assert (
result.text
== "You are a support assistant for Acme Robotics. Keep your replies under 50 words."
)
assert re.fullmatch(r"[0-9a-f]{64}", result.template_hash)
FieldValue
textThe rendered body text.
nameThe prompt’s name.
variantThe resolved variant name ("default" when none was selected).
template_hashSHA256(resolved variant template source) — lowercase hex.
render_hashSHA256(rendered output text) — lowercase hex.
guard`str
getting-started_python_error_types.py
"""Error types — a rejected render value raises PromptValidationError, whose
`.errors` are normalized FieldError rows (.field, .code, .message)."""
from pathlib import Path
from prompting_press import (
Prompt,
PromptingPressError,
PromptValidationError,
PromptRenderError,
LoadError,
)
from pydantic import BaseModel, field_validator
class AssistantVars(BaseModel):
company: str
max_words: int
@field_validator("max_words")
@classmethod
def _at_least_one(cls, v: int) -> int:
if v < 1:
raise ValueError("max_words must be at least 1")
return v
# The caller reads the definition; the library does no file I/O itself.
# Resolve the file next to this program (a real app uses its own path).
_HERE = Path(__file__).parent
assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text())
# The specific exceptions all derive from PromptingPressError.
assert issubclass(PromptValidationError, PromptingPressError)
assert issubclass(PromptRenderError, PromptingPressError)
assert issubclass(LoadError, PromptingPressError)
try:
result = assistant.render(
AssistantVars, data={"company": "Acme Robotics", "max_words": 0}
)
raise AssertionError("expected a validation error for max_words below 1")
except PromptValidationError as exc:
for row in exc.errors:
print(row.field, row.code, row.message)
# "max_words" "validation" "max_words must be at least 1"
row = exc.errors[0]
assert row.field == "max_words"
assert row.code == "validation"
assert "max_words must be at least 1" in row.message

The exception hierarchy:

PromptingPressError # base; carries .errors -> list[FieldError]
├── PromptValidationError # Pydantic validation failed (code "validation")
├── PromptRenderError # kernel render failure (codes: unknown_variant,
│ # undefined_variable, parse, render, excluded_feature)
└── LoadError # malformed YAML/JSON/TOML (code "load")

Each FieldError carries .field, .code, and .message. A rejected render value is never echoed onto the error surface; a parse error carries template-syntax detail (no bound values).

docs current as of 0.2.0