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.
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. Try it with a single line:
ad = generate_ad_copy("EcoBottle", "fitness enthusiasts", "Instagram", "motivational")
print(ad["headline"], "|", ad["cta"])
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.
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.
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.
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.
| Parameter | Type | Default here | Effect |
|---|---|---|---|
model | string | gpt-4o-mini | Which model answers. gpt-4o-mini is fast and cheap; gpt-4o is stronger for nuanced copy. |
temperature | float | 0.4 | Randomness of wording. Near 0 gives consistent, on-brand copy; higher gives more variety. |
response_format | object | json_object | Forces valid JSON so your code can read the result without text cleanup. |
Troubleshooting
json.JSONDecodeErrorwhen 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. Keepresponse_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.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.- A 401 error before any output appears. Your key was not loaded. Confirm the
.envfile sits next to the script, the variable is spelledOPENAI_API_KEY, andload_dotenv()runs before the client is created. Step through it with Fix the 401 Unauthorized Error in OpenAI Python. - 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.