Concepts / Organizing and Versioning Prompts

Organizing and Versioning Prompts

Keeping prompts in their own file, such as prompts.py, lets the instructions be named and referenced rather than embedded in function bodies.

  • Programming

A Function That Hides Its Instructions

Most small programs that call a language model start the same way. The author writes the instruction text directly inside the code that makes the call. This works well for a first attempt, but it quietly ties two different kinds of material together: the logic that calls the model, and the words that tell the model what to do. This article starts from a concrete function and asks what changes when those two parts are kept apart.

What do you think happens?

When you read a function that sends a request to a model, where would you look to find out what the model is being asked to do?

  • Inside the function body, in the text of the instructions

  • In the function's parameters, as an argument passed in by the caller

  • In a separate file that the function imports by name

Reveal answer
Answer:

The instructions are not an argument. In the source's example they are read from a named constant, GPT4o_ENTITY_EXTRACTION_PROMPT, which is the name that appears in the system message. The function never receives the instructions as input, so a reader has to look elsewhere to learn what the model is asked to do.

The parameter the caller supplies is only the text that changes from call to call. The fixed instructions live somewhere the function refers to by name.

Tracing a Query Function Step by Step

A model query in this style is built from a list of messages. Each message has a role and some content. A system message carries the standing instructions, and a user message carries the text that changes from one call to the next. The function below is a generated illustration of that principle. It pulls the message-building step into its own small function so you can inspect the result without calling a model. The instructions are not typed here; they are referred to by the name GPT4o_ENTITY_EXTRACTION_PROMPT, which will be defined in another file.

python
Notes on the code
  • Line 1 imports the named constant. The instruction text enters the application here.
  • The user_prompt parameter is the only input that changes from call to call.
  • The system message refers to the constant by name rather than containing its text.
  • The user message carries the caller's text in the role labeled user.

Trace the call build_messages('Ada Lovelace visited London.'). Python first runs the import on line 1, so the name GPT4o_ENTITY_EXTRACTION_PROMPT is available. Then it builds the list. The system entry takes its content from the constant. The user entry takes its content from the argument. The function returns the list, and the model query would send that list. Every step reads a value; none of them contains the instruction text itself.

prompts.pydefines the constantGPT4o_ENTITY_EXTRACTION_PROMPTinstruction textapplication fileimportfrom prompts importbuild_messagesfunctionquery functionsystem messagerole: systemmodel querylist of messagesuser messagerole: user, from argumentcontainsimported by nameused insidetakes constanttakes user_promptincludedincluded
Which code path carries the instruction text from prompts.py into the function that builds the model query, and in what order do those steps run?

Moving Instructions Into a Named Constant

The source recommends placing prompts in their own file, named prompts.py, and referring to them when a model query is created. The application file imports the prompt it needs by name, and the constant itself lives in prompts.py as a value such as GPT4o_ENTITY_EXTRACTION_PROMPT. The wording of the instructions has a single home, and the application only refers to it.

python
Before
def build_messages(user_prompt):
    return [
        {'role': 'system', 'content': 'Extract the names of people, places, and organizations from the text. Return them as a comma-separated list.'},
        {'role': 'user', 'content': user_prompt},
    ]

The instruction text is embedded in the function body.

After
from prompts import GPT4o_ENTITY_EXTRACTION_PROMPT

def build_messages(user_prompt):
    return [
        {'role': 'system', 'content': GPT4o_ENTITY_EXTRACTION_PROMPT},
        {'role': 'user', 'content': user_prompt},
    ]

The function refers to a named constant defined in prompts.py.

Notice what this does to the function. The query function still uses the same name in its system message, so the logic of the call is not rewritten. What has changed is where the text lives. If the instructions need revising, you open prompts.py rather than searching through application logic. The function's behavior is unchanged, but its dependence on the wording is now visible in one import line.

function bodycontains the instructionsinstruction textwritten inlineprompts.pyowns the wordingGPT4o_ENTITY_EXTRACTION_PROMPTnamed constantbuild_messagesfunctionrefers to the nameholdsdefinesreferenced by
How does the program's structure change when the instruction text moves out of the function body and into a named variable in prompts.py?

Reusing One Prompt Across Applications

The first advantage the source names is reuse. Once a prompt is a named value in its own module, any application that needs it can import it. The wording is written once and referenced from several places, so it is not copied into each program. Several applications can import the same prompt, and the wording lives in a single location.

prompts.pysingle source of thewordingGPT4o_ENTITY_EXTRACTION_PROMPTthe shared constantapplication oneimports the constantapplication twoimports the constantapplication threeimports the constantdefinesimportsimportsimports
Which applications import the same prompt object, and what would have to change in each one if the prompt text were edited?

Read the diagram as a question about edits. If the instruction text changes in prompts.py, each application that imports the constant picks up the new wording without its own copy being edited. The applications themselves do not need to change, because they only refer to the name. This is the practical meaning of writing the wording once.

Testing Code and Prompts Separately

The second advantage is that code and prompts can be tested separately. The source gives a concrete form of this: code can be tested with different prompts. When the prompt is embedded in the function, every test of the function is also a test of that particular wording. When the prompt is a separate value, a test can supply other wordings and check how the code behaves around each one.

python
Notes on the code
  • The first test checks the code's handling of the changing input, and does not depend on the wording of the instructions.
  • The second test checks that the system message is taken from the named constant.
Worked example

Isolating the cause of a wrong result

A run of the application returns poor entity lists. Is the cause the code or the prompt?

  1. 1
    Hold the code fixed

    Keep build_messages and the calling code exactly as they are.

  2. 2
    Vary the prompt

    Replace the constant in prompts.py with a revised wording and rerun the same inputs.

  3. 3
    Compare results

    If the output improves, the wording was the cause. If it does not, the wording was not the only problem.

  4. 4
    Hold the prompt fixed

    If the code had been changed instead, keep GPT4o_ENTITY_EXTRACTION_PROMPT unchanged and vary only the code to see its effect.

Result

Separation makes each experiment possible without rewriting the other half every time.

Who Benefits on a Team

The last advantage the source names concerns people rather than machines. Separating prompts from code allows subject matter experts to collaborate and help devise prompts without being distracted by code. Someone who knows the domain can open prompts.py, read and revise the instructions, and never need to follow the logic of the query function. Developers, in turn, can keep working in the application file without having the wording of every instruction in view.

The source also says that separation makes both files easier to read. Each file now has one main purpose, so a reader can tell at a glance whether a file is about behavior or about wording.

Mistakes Beginners Make With Prompt Files

  • Common mistake

    Keeping a copy of the instruction text inside the function as well as in prompts.py

    What it looks like

    The system message is typed out again inside build_messages, even though the constant already exists.

    Why it fails

    Two copies of the wording can drift apart, and edits to prompts.py will silently fail to reach the function.

    Do this instead

    Refer to the constant by name in the function and remove the inline copy.

  • Common mistake

    Passing the instructions in as a parameter when they are fixed

    What it looks like

    build_messages(instructions, user_prompt) is called with the same instruction text every time.

    Why it fails

    The parameter list now reflects a choice that never varies, which hides the fact that the instructions are a fixed part of the program.

    Do this instead

    Keep user_prompt as the only input that changes from call to call, and import the fixed instructions by name.

  • Common mistake

    Assuming a test of the function proves the wording is good

    What it looks like

    A test passes because the function returns the right structure, and the author concludes the instructions are effective.

    Why it fails

    The test checked the code's behavior, not the quality of the instructions that the model received.

    Do this instead

    Test the code with its handling of inputs, and evaluate prompt wording as a separate question by varying only the prompt.

  • Common mistake

    Putting unrelated helper functions in prompts.py, which blurs the one-purpose separation the source describes

Keeping Prompt Changes Traceable

Because the wording now lives in one named place, that place is also the natural point to record how the instructions have changed over time. The source material for this article concentrates on why separation helps and does not describe a specific versioning method, so this section does not prescribe one. What the separated structure does give you is a single file whose history answers the question of what wording a given program used. Keep the instruction text in one place, change it deliberately, and make each change reviewable on its own.

Checking Your Understanding of the Separated Design

MEDIUM

A colleague has a function that embeds a long instruction string directly in its body, and two other scripts contain nearly identical copies of that same string. Describe how you would move the wording into prompts.py, which parts of the code you would change, and which parts you would leave alone. Then explain which of the three copies would disappear and what each application would import.

Hints
  • Start by naming the constant, then identify the one line in each application that would change.
  • Think about which input still changes from call to call, since that input should remain a function parameter.
  • Consider what a change to the wording would require after the move.

Key Takeaways

  • Embedding instructions inside a function ties the wording to the logic that calls the model, so the instructions are hidden from anyone reading the function.
  • Keeping prompts in their own file such as prompts.py lets the application refer to a named constant, so the function's code is unchanged while the wording has a single home.
  • Several applications can import the same named prompt, so the wording is written once and reused rather than copied.
  • Code and prompts can be tested and revised separately, and subject matter experts can work on prompts without reading the code.

Related concepts

More concepts starting with O