This guide shows you how to turn a single keyword into a full, edited-ready Markdown blog post in about fifteen minutes, using Python and the OpenAI API. You will build a small script that works in four stages: outline, draft, refine, and save. Splitting the work this way gives you far more reliable results than asking the model to "write a blog post" in one shot.
If you have never called an AI model from code before, the Understanding LLM APIs section explains the basics first. Otherwise, read on.
Prerequisites
You only need three things beyond a working Python 3.10 or newer install:
- An OpenAI account and an API key (a secret string that authorizes your requests). Create one in the OpenAI dashboard under API keys.
- A folder to work in, with a virtual environment so your packages stay isolated. If you have not made one before, follow Create a Python Virtual Environment for AI.
- The two packages this script uses, installed into that environment:
pip install openai python-dotenv
The openai package is the official SDK (software development kit) that talks to the API. python-dotenv loads secrets from a file so you never paste your key directly into code.
Step 1: Set up your environment and key
Create a file called .env in your project folder and put your key inside it. The .env file holds secrets that should never appear in your code:
OPENAI_API_KEY=sk-your-real-key-goes-here
Now add .env to your .gitignore file so the key is never committed to version control or pushed to a public repository:
echo ".env" >> .gitignore
That one line prevents the most common way beginners accidentally leak a paid API key. Next, create a file called blog_writer.py and load the key into a client object. The client is the gateway you call for every request:
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv() # reads the .env file into environment variables
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
MODEL = "gpt-4o-mini" # cheap and fast; swap for "gpt-4o" if you need stronger writing
If this line raises an authentication error, your key is missing or wrong. The Fix the 401 Unauthorized Error in OpenAI Python guide walks through every cause.
Step 2: Generate an outline
A good article needs a skeleton before it needs prose. Asking the model for an outline first means the draft follows a deliberate structure instead of rambling. Add this helper function, which returns a clean list of headings:
def generate_outline(topic: str, audience: str) -> str:
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"You are a content strategist. Reply with a blog post outline "
"as a Markdown list of H2 and H3 headings only. No prose."
),
},
{
"role": "user",
"content": (
f"Create an outline for a blog post about '{topic}' "
f"written for {audience}. Aim for 5 to 7 main sections."
),
},
],
temperature=0.6,
)
return response.choices[0].message.content
The system message sets the role and the rules; the user message carries your specific request. A slightly lower temperature (0.6) keeps the outline focused. For more on writing instructions that the model actually obeys, see Write System Prompts that Control Output Format.
Step 3: Draft the full post
Now feed the outline back to the model and ask it to expand each heading into a finished article. Passing the outline as context is what keeps the draft on track:
def generate_draft(topic: str, audience: str, outline: str) -> str:
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"You are an expert copywriter. Write in clear, plain English. "
"Output valid Markdown using ## and ### headings. "
"Start with a one-paragraph introduction and no title."
),
},
{
"role": "user",
"content": (
f"Write a 1,200-word blog post about '{topic}' for {audience}. "
f"Follow this outline exactly:\n\n{outline}\n\n"
"End with three actionable takeaways as a bullet list."
),
},
],
temperature=0.7,
max_tokens=2500,
)
return response.choices[0].message.content
Here temperature=0.7 gives the prose a little personality without going off the rails, and max_tokens=2500 leaves enough room for a long article. One token is roughly three-quarters of a word, so 2500 tokens covers a 1,200-word post comfortably. If your draft cuts off mid-sentence, raise max_tokens.
Step 4: Refine the draft
First drafts from any model tend to open with a generic sentence and pad the middle. A cheap second pass fixes that. This function asks the model to edit its own work, which is more effective than trying to get a perfect draft in one request:
def refine_draft(draft: str) -> str:
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"You are a ruthless editor. Tighten the writing, cut filler "
"and repetition, and rewrite a weak opening line. "
"Keep all Markdown headings. Return the full edited post."
),
},
{"role": "user", "content": draft},
],
temperature=0.4,
)
return response.choices[0].message.content
The low temperature=0.4 keeps the editor conservative so it tightens the text instead of rewriting your meaning. This refine step is the single biggest quality lever in the whole script.
Step 5: Save the post to a file
Finally, write the finished Markdown to disk with a clean filename derived from the topic. Validating that the response is not empty protects you from saving a blank file after a failed call:
from pathlib import Path
def save_post(text: str, topic: str) -> Path:
if not text.strip():
raise ValueError("Empty response from the API; nothing to save.")
slug = topic.lower().strip().replace(" ", "-")
Path("output").mkdir(exist_ok=True)
path = Path(f"output/{slug}.md")
path.write_text(text, encoding="utf-8")
return path
Now wire the four stages together at the bottom of the file and run it:
if __name__ == "__main__":
topic = "Python automation for marketers"
audience = "non-technical marketing managers"
outline = generate_outline(topic, audience)
draft = generate_draft(topic, audience, outline)
final = refine_draft(draft)
saved_to = save_post(final, topic)
print(f"Saved post to {saved_to}")
Run it from your terminal:
python blog_writer.py
You will find a polished Markdown file inside the output folder, ready for you to read, fact-check, and publish.
Key parameter quick reference
These are the settings you will adjust most often. Tune the temperature per stage rather than using one value everywhere.
| Parameter | Type | Default here | Effect |
|---|---|---|---|
model | string | gpt-4o-mini | Picks the engine. gpt-4o-mini is cheapest; gpt-4o writes better but costs more. |
temperature | float | 0.4–0.7 | Controls randomness. Lower means consistent and safe; higher means creative and varied. |
max_tokens | int | 2500 | Caps the length of the reply. Raise it if drafts get cut off; lower it to save money. |
Troubleshooting
- The draft is much shorter than 1,200 words. Models treat word counts as a loose target. Generating section by section helps, but the reliable fix is to raise
max_tokensand to ask explicitly for "at least 1,200 words" in the prompt. RateLimitErroror a 429 message. You sent requests too fast or hit your spending cap. Add a short pause between calls, or follow Fix the 429 Rate-Limit Error in Python to add automatic retries.- **The output is wrapped in a
markdown code fence.** Some models wrap the whole reply in a fence. Strip it before saving with `text.strip().removeprefix("markdown").removesuffix("```").strip()`. - The post sounds generic and bland. That is almost always a weak prompt, not a weak model. Give the model a specific audience, a point of view, and concrete examples to include. The Prompt Engineering Templates for Marketers guide has ready-made starting points.
When to use this vs. alternatives
- Use this script when you write one article at a time, want full control over structure and tone, and plan to edit before publishing. The outline-draft-refine flow gives the best quality for a single piece.
- Reach for a batch approach when you need dozens of short, similar pieces, such as rewriting a catalog. Bulk-Rewrite Product Descriptions with Python loops over a list instead of building one long article.
- Pick a different format when your output is not a long-form post. For a recurring email, Generate Email Newsletters with Python and AI uses a shorter, section-based template that fits an inbox better.
Once this works, fold it into your broader AI Copywriting Workflows so a keyword list flows straight into finished drafts.
Back to AI Copywriting Workflows.
Related guides
- AI Copywriting Workflows — the section this guide belongs to.
- Bulk-Rewrite Product Descriptions with Python — run the same idea across a whole catalog.
- Generate Email Newsletters with Python and AI — adapt the flow for inbox-ready content.
- Write System Prompts that Control Output Format — get more predictable Markdown out of the model.