Fundamentals

Prompt Engineering Templates for Marketers

Build reusable Python prompt templates for ads, emails, and social posts. Fill variables, call the OpenAI API, and save consistent outputs every run.

This guide shows you how to build reusable prompt templates in Python that turn out ad copy, email sequences, and social posts on demand, in well under an hour. If you have ever retyped the same prompt into a chat box with small tweaks, then copied the answer into a spreadsheet by hand, this replaces all of that with a script you run once per campaign.

A template is just instruction text with blanks in it, like the product name or the platform, that you fill with real values in code. Because the wording stays fixed and only the specifics change, every output comes back in the same shape and style. That consistency is what lets you feed the results straight into your scheduler or content calendar instead of cleaning up each one by hand. You will write three templates, fill them from Python, call the OpenAI API, and save everything to a file you can open in any spreadsheet.

Prerequisites

You need Python 3.10 or newer and an OpenAI API key. If Python is not set up yet, work through Setting Up Python for AI first, and if API keys and requests are new to you, Understanding LLM APIs covers them from scratch. The broader Prompt Engineering Basics section explains the ideas behind the templates below.

Create a project folder and an isolated environment so these packages do not collide with anything else on your machine. A virtual environment is a private copy of Python that only this project uses.

python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install openai python-dotenv

Store your API key in a file named .env in the project root rather than pasting it into the script, so the key never ends up in your code:

OPENAI_API_KEY=your_api_key_here

Add .env to your .gitignore so the key is never committed to version control. A leaked key can be used by anyone to run up charges on your account.

Step 1: Set up the client and a reusable API caller

Every template will send a prompt through the same path, so write that path once. The code below loads your key, creates the OpenAI client, and wraps the API call in a small helper that asks for JSON and retries briefly if the network hiccups. JSON mode (the response_format setting) forces the model to return valid JSON, so reading the result back never trips over a stray sentence.

import os
import json
import time
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))


def run_prompt(prompt: str, system: str = "Return ONLY valid JSON.",
               temperature: float = 0.4, retries: int = 2) -> dict:
    """Send one filled prompt and return the parsed JSON object."""
    for attempt in range(retries):
        try:
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[
                    {"role": "system", "content": system},
                    {"role": "user", "content": prompt},
                ],
                response_format={"type": "json_object"},
                temperature=temperature,
                timeout=30,
            )
            return json.loads(response.choices[0].message.content)
        except Exception as error:
            if attempt == retries - 1:
                raise
            print(f"Retrying after error: {error}")
            time.sleep(2)

The system message sets the rules for every reply, the temperature controls how random the wording is, and the prompt carries the specifics. Keeping all three as arguments means each template can adjust them without rewriting the call. Temperature is the dial you will reach for most often once the templates are running, and What Is Temperature in an LLM API? explains what the numbers actually do to your copy.

Everything on this page rides on that one helper, so it is worth seeing the whole path a prompt takes before you write the templates that feed it.

The path one filled prompt takes from template to saved CSV row A data-flow diagram in two rows: a template plus your values enters the run_prompt helper, which calls gpt-4o-mini with JSON mode on; the JSON reply comes back, json.loads turns it into a Python dictionary, and csv.DictWriter writes it to a file. Template + values product, audience run_prompt() one shared helper gpt-4o-mini JSON mode on JSON reply headline, body, cta Python dict json.loads parses CSV file csv.DictWriter
Every template on this page joins the loop at the top left; only the wording of the prompt changes, so the four steps after it never have to be rewritten.

Step 2: Write a reusable ad copy template

A template is a regular Python string with placeholders. Here an f-string (a string prefixed with f that lets you drop variables straight into the text using curly braces) injects the product, audience, platform, and tone. The function returns a ready-to-use ad with a headline, body, and call to action.

def generate_ad_copy(product_name: str, target_audience: str,
                     platform: str, tone: str) -> dict:
    prompt = f"""Write one {platform} ad for '{product_name}'.
Audience: {target_audience}.
Tone: {tone}.
Only use facts implied by the product name; do not invent prices or claims.
Return a JSON object with keys: 'headline', 'body', 'cta'."""
    return run_prompt(prompt)

Because the structure is fixed, you can call this for any product on any platform and always get the same three fields back. If the voice still drifts away from your brand, paste two or three of your best past ads into the template as examples; Few-Shot Prompting in Python with Examples shows how to lay those examples out so the model copies the style rather than the content. Try it with a single line:

ad = generate_ad_copy("EcoBottle", "fitness enthusiasts", "Instagram", "motivational")
print(ad["headline"], "|", ad["cta"])

In that one line the placeholders disappear and your arguments take their place, which is the entire trick behind a template. Compare the text you wrote with the text the model actually receives:

The same prompt before and after the f-string fills its blanks A before-and-after comparison: the left column shows three curly-brace placeholders in the template text, and the right column shows the concrete values Python substitutes when you call the function. Before After {platform} blank in the template {product_name} filled from a variable {target_audience} who the ad speaks to Instagram chosen per campaign EcoBottle the product you sell fitness fans the segment you want f-string fill
Only the right-hand column changes between runs, so the instructions that make the copy good stay word-for-word identical across every product you generate.

Step 3: Build an email sequence template

Some assets are made of several pieces. A drip sequence is a set of emails sent over days, each building on the last. The template stays the same; a loop fills it once per email and passes the position so the model knows where each message sits in the series.

def build_email_sequence(campaign_goal: str, customer_segment: str,
                         sequence_length: int) -> list[dict]:
    emails = []
    for position in range(1, sequence_length + 1):
        prompt = f"""Write email {position} of a {sequence_length}-part drip sequence.
Goal: {campaign_goal}.
Audience segment: {customer_segment}.
Return a JSON object with keys: 'subject', 'preview_text', 'body'."""
        emails.append(run_prompt(prompt))
    return emails

Passing position and sequence_length into the prompt gives the model the context it needs to vary each email, so the first one welcomes the reader and the last one pushes for the sale. Each call here is still independent; when you want email three to quote a line from email two, feed the earlier output back in as described in Chain Prompts Together in Python.

Step 4: Build a social calendar template

JSON mode always returns a single top-level object, so when you want a list of posts, ask the model to wrap that list inside a named key and pull it out after parsing. This template produces a full calendar with text, hashtags, an image idea, and a date for each post.

def generate_social_calendar(niche: str, post_count: int,
                             content_themes: list[str]) -> list[dict]:
    prompt = f"""Create a {post_count}-post social calendar for the {niche} niche.
Themes to rotate through: {', '.join(content_themes)}.
Return a JSON object with one key 'posts' whose value is an array of objects,
each with keys: 'post_text', 'hashtags', 'image_idea', 'scheduled_date'."""
    result = run_prompt(prompt)
    return result.get("posts", [])

The ', '.join(content_themes) turns your list of themes into a comma-separated string the model can read, so you pass themes as a normal Python list and the template handles the formatting.

That is all three templates written. They differ only in what you hand them and what comes back, and seeing them side by side tells you which one to call when a brief lands on your desk:

The three templates compared by inputs and returned JSON keys A comparison matrix with three rows, one per template — ad copy, email sequence and social calendar — listing the values you pass in and the JSON keys each one hands back. Template You fill in You get back Ad copy Step 2 function product, audience platform, tone one JSON object headline, body, cta Email sequence Step 3 function goal, segment sequence length a list of emails subject, preview, body Social calendar Step 4 function niche, post count content themes a list of posts text, tags, date
Two of the three templates return a list, which is why the CSV helper in the next step takes a list of dictionaries rather than a single result.

Step 5: Save the outputs to a file

Generated copy is only useful once it leaves the script. This helper writes any list of results to a CSV that opens in Excel, Google Sheets, or most scheduling tools. The __main__ block ties the whole flow together: generate, then save.

import csv


def export_to_csv(rows: list[dict], filename: str = "marketing_outputs.csv") -> None:
    if not rows:
        print("Nothing to save.")
        return
    with open(filename, "w", newline="", encoding="utf-8") as file:
        writer = csv.DictWriter(file, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)
    print(f"Saved {len(rows)} rows to {filename}")


if __name__ == "__main__":
    ads = [generate_ad_copy("EcoBottle", "fitness enthusiasts", "Instagram", "motivational")]
    export_to_csv(ads, "ads.csv")

    emails = build_email_sequence("trial signups", "new subscribers", 3)
    export_to_csv(emails, "emails.csv")

    calendar = generate_social_calendar("sustainable fitness", 7,
                                        ["product tips", "customer stories", "behind the scenes"])
    export_to_csv(calendar, "calendar.csv")

Run the file with python your_script.py. You will get three CSV files, one per asset type, each ready to review and import. A run like this one costs a fraction of a cent, but a catalogue of five hundred products is a different matter, so price the job before you launch it with Estimate OpenAI API Costs with Python.

Key parameter quick reference

These are the settings on the API call that most affect your marketing output. Adjust them in run_prompt or pass them per template.

ParameterTypeDefault hereEffect
modelstringgpt-4o-miniWhich model answers. gpt-4o-mini is fast and cheap; gpt-4o is stronger for nuanced copy.
temperaturefloat0.4Randomness of wording. Near 0 gives consistent, on-brand copy; higher gives more variety.
response_formatobjectjson_objectForces valid JSON so your code can read the result without text cleanup.

Troubleshooting

  1. json.JSONDecodeError when reading the result. The model returned text that is not valid JSON, usually because JSON mode was off or the system message did not ask for JSON. Keep response_format={"type": "json_object"} on the call and the words "Return ONLY valid JSON" in the system message. The deeper fixes are in Fix JSONDecodeError with AI API Responses in Python.
  2. KeyError: 'posts' from the calendar function. The model wrapped its list under a different key name. The .get("posts", []) already returns an empty list instead of crashing, but if it keeps happening, repeat the exact key name 'posts' in the prompt so the model cannot drift.
  3. A 401 error before any output appears. Your key was not loaded. Confirm the .env file sits next to the script, the variable is spelled OPENAI_API_KEY, and load_dotenv() runs before the client is created. Step through it with Fix the 401 Unauthorized Error in OpenAI Python.
  4. A 429 error when generating long sequences. Too many calls landed too quickly for your account tier. The loop in the email and calendar templates sends one request at a time, so add a short time.sleep(1) between calls, or follow Fix the 429 Rate-Limit Error in Python for a proper backoff.

When to use this vs. alternatives

  • Use Python templates when you generate the same kind of asset over and over, such as ads for a catalogue of products or a weekly social calendar. The cost of writing the script pays off the second time you run it, and every output stays consistent.
  • Use a chat window when you are exploring ideas for a single one-off piece and the wording matters more than repeatability. There is no point scripting something you will write once.
  • Reach for stricter output control when the shape of the result has to be exact, for example feeding a database. Pair these templates with the validation approach in Write System Prompts that Control Output Format so missing fields are caught before they reach your other tools.

Once the three templates run cleanly, treat them as assets rather than scripts: keep them in one file, version the wording the way you would version a landing page, and change one variable at a time so you can tell what improved the copy. The hour you spend here is the last hour you spend retyping a prompt. Back to Prompt Engineering Basics.

Frequently asked questions

What is a prompt template?

A prompt template is a reusable block of instruction text with blanks in it, like the product name or the platform. You fill the blanks with real values in your code, so every prompt has the same proven structure and only the specifics change. It turns a one-off message into something you can run a hundred times.

Why use Python instead of typing prompts into ChatGPT?

Typing prompts by hand is slow and inconsistent, and you cannot save or reuse the wording. A Python script fills the same template, calls the API, and writes the results to a file in seconds. You get repeatable outputs and a record of exactly what was sent.

How do I get the same style of output every time?

Lower the temperature setting toward 0 so the model is less random, put your formatting rules in the system message, and ask for JSON so the shape is fixed. Together these three things make the output predictable enough to drop straight into a spreadsheet.

Which OpenAI model should marketers use for copy?

gpt-4o-mini is fast and cheap and handles ad copy, emails, and social posts well, so it is the right default for high-volume marketing work. Move up to gpt-4o only if you need noticeably stronger reasoning or longer, more nuanced pieces.

How do I stop the model from inventing fake claims?

Tell it in the system message to only use facts you provide, and pass any real details, such as price or features, as variables in the prompt. Keep temperature low and always review generated claims before publishing.