cd /news/ai-tools/stop-hardcoding-prompts-in-your-pyth… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-155268] src=dev.to β†— pub= topic=ai-tools verified=true sentiment=↑ positive

Stop Hardcoding Prompts in Your Python Files: Meet PromptFill (best balance of curiosity and search terms)

A developer released PromptFill, a zero-dependency Python package that loads prompt templates from .md or .txt files and substitutes $placeholder variables at runtime using the standard library's string.Template. The tool raises FileNotFoundError for missing files and ValueError for missing variables, and was designed to avoid treating curly braces in prompts as placeholders.

by read5 min views2 publishedOct 22, 2026

Open any LLM-powered Python project that's been alive for a few weeks and you'll probably find the same thing: a function with a prompt inside it. Then the prompt grows a few more instructions, a formatting rule, an example. Before long, the logic is buried under a wall of text and nobody wants to touch the file.

That's the itch behind PromptFill, a small, zero-dependency Python package that lets you keep prompts in their own files and fill in the variables at runtime.

pip install promptfill

This post covers what it does, how it works under the hood (it's short enough to read in a minute), and where it fits.

Prompts aren't really code, but we treat them like code because they happen to be strings. That causes predictable problems:

Think of a form letter. Nobody retypes the whole letter for each recipient. You keep one template with blanks like Dear ___, and fill in the names when it's time to send. Prompts deserve the same treatment: one template file, blanks filled in by your code.

PromptFill loads a prompt template from a text file, replaces the placeholders with values you pass from Python, and returns a plain string. That's the whole job, and it's deliberately small.

.md or .txt file.load_prompt() and supply those values. What you get:

ValueError FileNotFoundError Create reviewer.md:

You are an expert $role.

Review the following code written in $language for bugs,
security vulnerabilities, and performance issues.

Code to review:

$code_snippet

Provide your findings and suggest improvements.

Placeholders use a $ prefix: $role, $language, $code_snippet.

from promptfill import load_prompt

prompt = load_prompt(
    file_path="reviewer.md",
    role="Senior Security Engineer",
    language="Python",
    code_snippet="print('Hello, World!')"
)

print(prompt)
You are an expert Senior Security Engineer.

Review the following code written in Python for bugs,
security vulnerabilities, and performance issues.

Code to review:

print('Hello, World!')

Provide your findings and suggest improvements.

The return value is a regular Python string, so it drops straight into whatever client you already use.

The whole implementation fits on one screen:

from pathlib import Path
from string import Template
from typing import Union

def load_prompt(file_path: Union[str, Path], **kwargs) -> str:
    path = Path(file_path)

    if not path.is_file():
        raise FileNotFoundError(f"Prompt file not found at: {file_path}")

    content = path.read_text(encoding="utf-8")
    template = Template(content)

    try:
        return template.substitute(**kwargs)
    except KeyError as e:
        raise ValueError(f"Missing prompt variable for placeholder: {e}")

Here's what happens when you call it:

string.Template from the contents.$placeholder substitution, which is why placeholders look the way they do.substitute() with your keyword arguments.$name in the template is replaced with the matching value.Template raises a KeyError internally, and PromptFill catches it and re-raises it as a I chose string.Template over f-strings or str.format() on purpose. It only cares about $, so curly braces in your prompt (a JSON example, say) aren't treated as placeholders. And since it's part of the standard library, there's nothing extra to install, audit, or keep updated.

The real payoff comes when one prompt serves several cases:

from promptfill import load_prompt

template_path = "reviewer.md"

python_prompt = load_prompt(
    file_path=template_path,
    role="Python Developer",
    language="Python",
    code_snippet="print(10 + 20)"
)

javascript_prompt = load_prompt(
    file_path=template_path,
    role="JavaScript Developer",
    language="JavaScript",
    code_snippet="console.log(10 + 20)"
)

No duplicated text and no drift between copies. Improve the wording in reviewer.md and every caller benefits.

Say your template expects $role, $language, and $code_snippet, but you forget language:

from promptfill import load_prompt

prompt = load_prompt(
    file_path="reviewer.md",
    role="Developer",
    code_snippet="x = 1"
)

You get:

ValueError: Missing prompt variable for placeholder: 'language'

It fails loudly, and it names the exact variable you forgot. That beats sending an incomplete prompt to a model and wondering why the output is strange.

Because PromptFill builds on string.Template, a few of its standard behaviors carry over:

$ in a prompt (for example, a price), write $$ in the template file.${name} also works.${language}_review. PromptFill only formats prompts. It doesn't call any model itself, so you're free to use whichever provider you like. Here's an example with an OpenAI-compatible client:

from promptfill import load_prompt
from openai import OpenAI

client = OpenAI()

prompt = load_prompt(
    file_path="reviewer.md",
    role="Python Expert",
    language="Python",
    code_snippet="print('Hello, World!')"
)

response = client.chat.completions.create(
    model="YOUR_MODEL_NAME",
    messages=[{"role": "user", "content": prompt}]
)

print(response.choices[0].message.content)

The OpenAI SDK is installed separately (pip install openai) and only for this example. PromptFill itself stays dependency-free.

Once prompts are files, organizing them is easy:

my-ai-project/
β”œβ”€β”€ prompts/
β”‚   β”œβ”€β”€ reviewer.md
β”‚   β”œβ”€β”€ summarizer.md
β”‚   └── chatbot.txt
β”œβ”€β”€ main.py
β”œβ”€β”€ requirements.txt
└── README.md

One file per prompt, named for its purpose. It also makes prompt changes easy to review: a diff on prompts/summarizer.md reads like a diff on a document, not a tangle of string literals.

PromptFill is aimed at anyone who keeps prompts in Python projects:

PromptFill is a text formatter. It doesn't make prompts or inserted values safe on its own, so a few habits still apply:

PromptFill is young and the roadmap is open. Ideas I'm considering:

If one of these matters to you, tell me. Feedback shapes what gets built first.

pip install promptfill

If you try it, I'd love to hear how it goes. Open an issue, suggest a feature, or send a pull request. And if it saves you from one more 80-line string in a Python file, a star on the repo goes a long way.

How do you manage prompts in your projects: inline strings, files, or something else? Let me know in the comments.

── more in #ai-tools 4 stories Β· sorted by recency
── more on @promptfill 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/stop-hardcoding-prom…] indexed:0 read:5min 2026-10-22 Β· β€”