You type a question into a large language model (LLM) and get back a wall of generic mush. Your coworker asks what looks like the same thing and gets a clean, structured answer. Same model, different prompt. Python prompt engineering is the craft of closing that gap: writing instructions that pull sharp, usable output from a model, then wiring them into real code.
Think of the model as a brilliant new intern on their first day. Smart, fast, knows a ton, but takes you completely literally and cannot read your mind. If you say “tell me about decorators,” you get a rambling essay. If you say “explain decorators in 3 bullets for someone who knows functions, include one code snippet,” you get exactly that. This is not about finding magic words. It is about saying clearly what you want, in a shape the model can follow.
“The art of prompting is not about tricking the model. It is about communicating clearly with a very literal listener.”
Lilian Weng, OpenAI
Last Updated: July 2026 | Tested on: Python 3.14.6, openai 2.43.0 | Difficulty: Intermediate | Reading Time: 26 minutes
- large language models tutorial
- An OpenAI API key (or any LLM SDK) to run the API examples yourself
This post is a recipe book. We will walk through the core prompting patterns one by one: zero-shot, few-shot, chain-of-thought, system prompts, and the advanced reasoning tricks (self-consistency and ReAct) that power real AI agents. Each pattern comes with copy-paste Python, the kind of output you can expect, and a note on when to reach for it. By the end you will write prompts that cut down on hallucinations, return structured output you can parse with code, and handle multi-step reasoning without falling apart.
One quick note before we start: the code in this post calls a real LLM Application Programming Interface (API), which needs an API key and costs a few cents per run. The outputs shown below are representative responses, labeled as illustrative, since model wording shifts between versions and runs. The Python itself is valid and ready to run with your own key. Model names also change fast, so always check the provider docs for the current one.
Table of Contents
Zero-Shot Prompting: No Examples Needed
Zero-shot means you hand the model a task and no examples. Think of asking a well-read friend for a book recommendation cold: you describe what you want and trust their general knowledge to fill the gap, without first handing them a sample list. The model leans entirely on what it already learned during training. This is the simplest pattern, and it works shockingly well for everyday tasks like classification, extraction, and short explanations. The whole game is being specific: spell out the task, the audience, and the exact output format you want. The snippet below runs the same request at three levels of specificity so you can feel the difference.
A quick word on that API key: an API key is a secret password that tells the provider which account a request belongs to, so it can identify you and bill the usage to the right place. Keep it in an environment variable named OPENAI_API_KEY rather than pasting it into your source, and never commit it to git where anyone reading the repo could grab it. The SDK looks for that variable on its own, which is why OpenAI() in the code below takes no key argument at all.
📄 zero_shot.py: clear instructions beat vague requests
from openai import OpenAI
client = OpenAI() # reads the OPENAI_API_KEY environment variable
# A current, low-cost model. Model names change fast, so keep this in one
# place and check the provider docs for the latest.
MODEL = "gpt-5.4-mini"
# Rahul compares vague vs specific zero-shot prompts
prompts = {
"Vague (bad)": "Tell me about Python decorators.",
"Specific (good)": (
"Explain Python decorators in exactly 3 bullet points. "
"Each bullet should be one sentence. "
"Target audience: intermediate Python developer who knows functions but not decorators. "
"Include one code snippet showing the @syntax."
),
"Structured (best)": (
"You are a Python instructor writing for developers with 1-2 years of experience.\n\n"
"Task: Explain Python decorators.\n"
"Format: 3 bullet points, each one sentence.\n"
"Include: One code snippet showing @decorator syntax.\n"
"Tone: Direct, no filler phrases like 'it is important to note'.\n"
"Output: Markdown format."
),
}
for label, prompt in prompts.items():
print(f"\n{'='*50}")
print(f" {label}")
print(f"{'='*50}")
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=300,
)
print(response.choices[0].message.content[:300])
print(f" Tokens used: {response.usage.total_tokens}")
▶ Output (illustrative, real API response will vary)
==================================================
Vague (bad)
==================================================
Python decorators are a very useful feature that allows you to modify
the behavior of functions or classes. They were introduced in Python 2.4
and have since become one of the most widely used patterns in Python
programming. Decorators can be used for logging, authentication,
caching, and many other purposes...
Tokens used: 287
==================================================
Specific (good)
==================================================
- A decorator is a function that takes another function as input and
returns a modified version of it, letting you add behavior (like
logging or timing) without changing the original function's code.
- You apply a decorator using the `@` syntax above a function definition:
```python
@my_decorator
def greet():
print("Hello")
```
- Under the hood, `@my_decorator` is equivalent to
`greet = my_decorator(greet)`.
Tokens used: 154
==================================================
Structured (best)
==================================================
- A decorator wraps a function to extend its behavior without modifying
its source code. Think of it as a reusable "before/after" hook.
- Apply it with `@` above the function definition:
```python
@timer
def process_data():
...
```
- Python executes `process_data = timer(process_data)` at definition
time, so the wrapped version replaces the original.
Tokens used: 142
What happened here: The vague prompt got a generic essay and burned the most tokens on filler. The specific prompt landed the right format but still left room for the model to wander. The structured prompt nailed the format, tone, audience, and length, and did it with the fewest tokens (142 vs 287, which means it was also the cheapest). See the pattern in the best prompt: role, explicit task, format, constraints, output type. That five-part skeleton works for almost any zero-shot job, so it is worth memorizing.
Few-Shot Prompting: Teaching by Example
When a plain instruction is not precise enough, show the model what you mean instead of telling it. Few-shot prompting drops 2 to 5 worked examples into the prompt, each one a paired input and output. The model spots the pattern and copies it on the next input, no fine-tuning required. It is the same way you would teach a new teammate a data-entry format: you do not write a spec, you just show them three filled-in rows and say “now you do the next one.” In day-to-day Python prompt engineering, few-shot is usually the first upgrade you reach for when zero-shot output starts drifting.
📄 few_shot.py: examples are worth a thousand instructions
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5.4-mini" # current low-cost model at the time of writing; check provider docs
# Niranjan uses few-shot for consistent entity extraction
few_shot_prompt = """Extract structured data from customer messages.
Example 1:
Input: "Hi, I'm Viraj (viraj@techno.com). I bought the Pro plan last week but I'm being charged for Enterprise."
Output: {"name": "Viraj", "email": "viraj@techno.com", "issue": "billing", "plan_mentioned": "Pro", "sentiment": "frustrated"}
Example 2:
Input: "Love the product! Aditi here, aditi@startup.io. Quick question, can I add 3 more team members to my Business plan?"
Output: {"name": "Aditi", "email": "aditi@startup.io", "issue": "account_management", "plan_mentioned": "Business", "sentiment": "positive"}
Example 3:
Input: "This is ridiculous. Prathamesh, prathamesh@dev.co. Your API has been down for 2 hours and we're losing customers."
Output: {"name": "Prathamesh", "email": "prathamesh@dev.co", "issue": "outage", "plan_mentioned": null, "sentiment": "angry"}
Now extract from this message:
Input: "Hey, Pravin here (pravin@data.io). The dashboard loads really slowly on the Team plan. Takes 30 seconds every time."
Output:"""
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": few_shot_prompt}],
temperature=0,
max_tokens=100,
)
print("Few-shot extraction result:")
print(response.choices[0].message.content)
▶ Output (illustrative, real API response will vary)
Few-shot extraction result:
{"name": "Pravin", "email": "pravin@data.io", "issue": "performance", "plan_mentioned": "Team", "sentiment": "frustrated"}
What happened here: Three examples were enough to teach the model our exact JSON schema, the field names, the allowed sentiment labels, and how to handle missing data (use null for the plan when nobody mentions one). The new message never said the word “performance,” yet the model mapped “loads really slowly” to that category and read the mild complaint as “frustrated.” It generalized the pattern instead of memorizing it. That is the appeal of few-shot: it sits right between zero-shot (fast but loose) and fine-tuning (tight but slow and costly to set up), and you can change the behavior just by editing the examples.
Chain-of-Thought: Making LLMs Think Step by Step
Chain-of-thought (CoT) prompting is the single biggest win for reasoning tasks. Instead of asking for the answer, you ask the model to show its work. Remember how your math teacher made you write every step instead of just the final number? Same idea, same reason. When the model spells out the intermediate steps, each step becomes context for the next one, and the final answer is far more likely to be right on math, logic, and anything multi-step.
📄 chain_of_thought.py: step-by-step reasoning lifts accuracy
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5.4-mini" # current low-cost model at the time of writing; check provider docs
# Viraj tests CoT on a tricky logic problem
problem = (
"Vinay has 3 boxes. Box A has 5 red balls and 3 blue balls. "
"Box B has 2 red balls and 6 blue balls. Box C has 4 red balls and 4 blue balls. "
"He moves all red balls from Box A to Box C, then moves half the blue balls "
"from Box B to Box A. How many balls are in each box now?"
)
# Without CoT
direct_response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": f"{problem}\n\nGive the answer directly."}],
temperature=0,
max_tokens=100,
)
# With CoT
cot_response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": (
f"{problem}\n\n"
"Let's solve this step by step:\n"
"1. First, list the initial contents of each box.\n"
"2. Then apply each move one at a time.\n"
"3. Show the state after each move.\n"
"4. Finally, count the totals."
)}],
temperature=0,
max_tokens=400,
)
print("--- Without Chain-of-Thought ---")
print(direct_response.choices[0].message.content)
print(f"\n--- With Chain-of-Thought ---")
print(cot_response.choices[0].message.content)
▶ Output (illustrative, real API response will vary)
--- Without Chain-of-Thought --- Box A: 6 balls, Box B: 5 balls, Box C: 13 balls --- With Chain-of-Thought --- Step 1: Initial state - Box A: 5 red + 3 blue = 8 balls - Box B: 2 red + 6 blue = 8 balls - Box C: 4 red + 4 blue = 8 balls Step 2: Move all red balls from Box A to Box C - Box A: 0 red + 3 blue = 3 balls - Box B: 2 red + 6 blue = 8 balls (unchanged) - Box C: 4+5 = 9 red + 4 blue = 13 balls Step 3: Move half the blue balls from Box B to Box A - Half of 6 blue = 3 blue balls moved - Box A: 0 red + 3+3 = 6 blue = 6 balls - Box B: 2 red + 6-3 = 3 blue = 5 balls - Box C: 9 red + 4 blue = 13 balls (unchanged) Final answer: - Box A: 6 balls (0 red, 6 blue) - Box B: 5 balls (2 red, 3 blue) - Box C: 13 balls (9 red, 4 blue)
What happened here: Both runs reached the same numbers this time, but only the CoT version showed work you can actually check. On harder problems the direct approach quietly breaks, because the model gambles on an answer with no steps to lean on. CoT makes it think out loud, and since each step feeds the next, small mistakes get caught instead of compounding. The original chain-of-thought paper (Wei et al., 2022, “Chain-of-Thought Prompting Elicits Reasoning in Large Language Models”) reported large accuracy jumps on math and reasoning benchmarks for big models.
One catch worth knowing: many of today’s frontier models already reason internally, so a blunt “think step by step” helps less than it used to. The pattern of asking for structured, checkable steps still pays off.
System Prompts: Setting the Stage
A system prompt sets the rules of the game before the user says a word. It runs once, up front, and shapes every reply for the whole conversation. This is the difference between a plain chatbot and a focused assistant. A good system prompt is like a job description you hand a contractor on day one: here is your role, here is what you do, here are the lines you do not cross, and here is the format I want everything delivered in. A surprising share of Python prompt engineering bugs trace back to a missing or mushy system prompt, so this section deserves your full attention.
📄 system_prompts.py: a production system prompt that actually works
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5.4-mini" # current low-cost model at the time of writing; check provider docs
# Rahul builds a code review assistant with a production system prompt
code_review_system = """You are a senior Python code reviewer at a tech company.
BEHAVIOR:
- Review code for bugs, performance issues, and Python best practices.
- Be direct. No praise sandwiches. If the code is bad, say so.
- Suggest specific fixes with corrected code, not vague advice.
- Rate severity: CRITICAL (will break), WARNING (should fix), STYLE (nice to have).
CONSTRAINTS:
- Only review Python code. Refuse other languages politely.
- Never generate new features. Only review what's given.
- Maximum 5 issues per review. Prioritize by severity.
OUTPUT FORMAT:
For each issue:
[SEVERITY] Line X: Brief description
Problem: What's wrong
Fix: Corrected code snippet
"""
code_to_review = '''
def get_user_data(user_id):
import requests
data = requests.get(f"http://api.example.com/users/{user_id}")
user = eval(data.text)
password = user['password']
cache = {}
cache[user_id] = user
return user
def process_users(ids):
results = []
for id in ids:
results.append(get_user_data(id))
return results
'''
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": code_review_system},
{"role": "user", "content": f"Review this Python code:\n```python\n{code_to_review}\n```"},
],
temperature=0.2,
max_tokens=600,
)
print("Code Review Output:")
print(response.choices[0].message.content)
▶ Output (illustrative, real API response will vary)
Code Review Output:
[CRITICAL] Line 4: Using eval() on network response is a remote code execution vulnerability
Problem: eval(data.text) executes arbitrary code from the API response.
Fix: user = data.json()
[CRITICAL] Line 5: Storing/accessing plaintext passwords
Problem: Reading user['password'] suggests passwords are transmitted in plaintext.
Fix: Remove password access. Passwords should never leave the auth service.
[WARNING] Line 2: Import inside function on every call
Problem: requests is imported every time get_user_data() runs.
Fix: Move `import requests` to the top of the file.
[WARNING] Line 6-7: Cache created locally, discarded on return
Problem: cache = {} is created fresh each call and never reused.
Fix: Use @functools.lru_cache or a module-level cache dict.
[STYLE] Line 11: Variable name shadows builtin `id`
Problem: `for id in ids` shadows Python's built-in id() function.
Fix: `for user_id in ids`
What happened here: The system prompt turned a general-purpose model into a focused code reviewer. It flagged a critical security hole (eval on data from the network), a plaintext password problem, a misplaced import, a useless local cache, and a name that shadows the built-in id, each one tagged with a severity level and formatted exactly as the prompt asked. Just as important, the constraints kept it on task: it did not drift into other languages or start inventing new features. Real production system prompts go much further than this, often hundreds or thousands of lines that bake in an entire company’s coding standards.
Advanced Techniques: Self-Consistency, ReAct, and Tree of Thoughts
Once basic chain-of-thought is in your toolkit, a few stronger patterns build on top of it. Two of them, self-consistency and ReAct (short for Reasoning and Acting), show up everywhere in real AI systems and agent workflows, so they are worth knowing by name.
📄 advanced_prompting.py: self-consistency and the ReAct pattern
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-5.4-mini" # current low-cost model at the time of writing; check provider docs
# 1. Self-Consistency: Ask the same question multiple times, take the majority answer
def self_consistent_answer(question, n_samples=3):
"""Generate multiple reasoning paths and vote on the answer."""
answers = []
for i in range(n_samples):
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": (
f"{question}\n\nThink step by step and give your final answer "
f"on the last line as 'ANSWER: '."
)}],
temperature=0.7, # Higher temp = diverse reasoning paths
max_tokens=300,
)
text = response.choices[0].message.content
# Extract the answer line
for line in text.strip().split("\n"):
if line.startswith("ANSWER:"):
answers.append(line.split("ANSWER:")[1].strip())
break
# Majority vote
from collections import Counter
vote = Counter(answers).most_common(1)[0]
return vote[0], vote[1], n_samples, answers
question = "If a shirt costs $25 after a 20% discount, what was the original price?"
answer, votes, total, all_answers = self_consistent_answer(question)
print(f"Question: {question}")
print(f"All answers: {all_answers}")
print(f"Consensus: {answer} ({votes}/{total} agreement)")
# 2. ReAct pattern: Reasoning + Action (used in AI agents)
react_prompt = """Answer the user's question using the ReAct pattern.
For each step:
Thought: What I need to figure out next
Action: What tool/calculation I would use
Observation: What the result would be
Continue until you reach the final answer.
Question: Pravin wants to deploy a Flask app. He has 1000 daily users,
each making ~50 API calls averaging 200ms response time.
Does he need more than one server instance?
"""
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": react_prompt}],
temperature=0.2,
max_tokens=500,
)
print(f"\n--- ReAct Pattern ---")
print(response.choices[0].message.content)
▶ Output (illustrative, real API response will vary)
Question: If a shirt costs $25 after a 20% discount, what was the original price? All answers: ['$31.25', '$31.25', '$31.25'] Consensus: $31.25 (3/3 agreement) --- ReAct Pattern --- Thought: I need to calculate the total daily API calls and how long they take. Action: Calculate total requests = 1000 users × 50 calls = 50,000 requests/day Observation: 50,000 requests per day Thought: I need to convert this to requests per second to understand server load. Action: Calculate RPS = 50,000 / 86,400 seconds ≈ 0.58 requests/second Observation: About 0.58 RPS average, but peak could be 5-10x higher = ~3-6 RPS Thought: With 200ms response time per request, how many concurrent requests? Action: At peak 6 RPS with 200ms each: 6 × 0.2 = 1.2 concurrent requests Observation: ~1-2 concurrent requests at peak Thought: A single Flask instance with a production WSGI server (gunicorn with 4 workers) can handle 20-50 concurrent requests easily. Action: Compare: need ~2 concurrent vs capacity of ~40 concurrent Observation: Single server is more than sufficient Final Answer: No, Pravin does not need more than one server instance. At peak load (~6 RPS, ~2 concurrent), a single server with gunicorn (4 workers) has roughly 20x headroom. He should monitor and scale if daily users grow past 10,000.
What happened here: Self-consistency asked the same question three times at a higher temperature, so each run reasoned a little differently, then it took the majority vote. All three landed on $31.25 (the original price is $25 / 0.80). Voting like this catches the odd run where the model slips, the same way you would trust three people who independently got the same answer over one person who is sure.
The ReAct pattern interleaves a Thought, an Action, and an Observation in a loop, which is exactly how AI agents (AI agents tutorial) work: think, act, look at what came back, repeat until done. A third idea, Tree of Thoughts, takes this further by exploring several branches of reasoning and backtracking from dead ends, which is heavier but useful for puzzles with many possible paths.
Prompt Chaining
Every example so far has been a single prompt doing a single job. Prompt chaining is what you reach for when the job is too big for one prompt to handle cleanly. Instead of asking the model to do everything at once, you break the work into a short pipeline of smaller prompts, where the output of step one becomes the input of step two, and so on down the line. It is the same idea as an assembly line: each station does one small thing to the piece in front of it and passes it along.
Say a customer sends a long, rambling support ticket. Step one is a prompt that reads the whole ticket and pulls out the key points: what broke, when it started, and how upset the customer sounds. Step two takes those key points (not the original wall of text) and drafts a polite reply that answers each one. Step three takes that draft and shortens it to three tight sentences. Three simple prompts, run in order, each one feeding the next.
Chaining helps for the same reasons you split a big function into small ones. Each step is simpler, so the model is more likely to get it right. Each step is easy to check on its own, so when the output looks off you can see exactly which stage went wrong instead of guessing. And you can fix or swap one step without redoing the rest, the same way you would edit a single function without rewriting the whole program. You will meet this pattern again later in the series, because AI agents and retrieval-based systems (RAG) are, underneath, chains of prompts wired together with a little Python between the steps.
Production Prompt Templates
Think of the saved reply templates a support team keeps, with blanks for the customer name and order number: nobody rewrites the whole message each time, they just fill the blanks. Production prompts work the same way. In a real codebase you do not scatter prompt strings across your files and tweak them by hand. You treat prompts like any other code: store them as versioned templates with named slots, fill the slots at runtime, and test them. The pattern below uses a small dataclass plus string.Template, and it has no API calls, so you can run it locally exactly as written. This is where Python prompt engineering earns the “engineering” half of its name.
📄 prompt_templates.py: reusable, testable prompt templates
from dataclasses import dataclass
from string import Template
# Anvi builds a prompt template system
@dataclass
class PromptTemplate:
name: str
version: str
system: str
user_template: str
def render(self, **kwargs):
return Template(self.user_template).safe_substitute(**kwargs)
# Define reusable templates
SUMMARIZE = PromptTemplate(
name="summarize",
version="1.2",
system=(
"You are a technical writer. Summarize content accurately. "
"Never add information not in the original. "
"Use bullet points. Maximum ${max_bullets} bullets."
),
user_template=(
"Summarize the following ${content_type} in ${max_bullets} bullet points.\n"
"Audience: ${audience}\n"
"Focus on: ${focus}\n\n"
"Content:\n${content}"
),
)
CLASSIFY = PromptTemplate(
name="classify",
version="2.0",
system=(
"You are a text classifier. Respond with ONLY the category name. "
"No explanation. Categories: ${categories}"
),
user_template="Classify this text: ${text}",
)
# Usage
summary_prompt = SUMMARIZE.render(
content_type="technical document",
max_bullets="5",
audience="Python developers",
focus="practical takeaways",
content="Large Language Models use transformer architecture with billions of parameters..."
)
print(f"Template: {SUMMARIZE.name} v{SUMMARIZE.version}")
print(f"Rendered prompt:\n{summary_prompt[:200]}...")
print(f"\n--- Template Registry ---")
for tmpl in [SUMMARIZE, CLASSIFY]:
print(f" {tmpl.name} v{tmpl.version}: system={len(tmpl.system)} chars, user={len(tmpl.user_template)} chars")
▶ Output (verified on Python 3.14.6)
Template: summarize v1.2 Rendered prompt: Summarize the following technical document in 5 bullet points. Audience: Python developers Focus on: practical takeaways Content: Large Language Models use transformer architecture with billions of p... --- Template Registry --- summarize v1.2: system=151 chars, user=134 chars classify v2.0: system=105 chars, user=27 chars
What happened here: We built versioned prompt templates with named slots. Notice the verified numbers: the registry reports system=151 and user=134 characters for the summarize template, computed straight from len(), not guessed. Once prompts live as objects like this, you can A/B test versions (v1.2 against v1.3), log which version produced which response, and edit a prompt without touching application logic. That is the whole point: prompts become first-class artifacts you can review and test, instead of magic strings buried in source code. Serious LLM products manage their prompts this way for exactly that reason.
The “Don’t Do This” Section
Every pattern has a flip side, and Python prompt engineering is full of habits that quietly waste money, return flaky output, or open security holes. Here are four of the most common, each with the fix. This snippet is plain Python with no API calls, so it runs locally as is.
📄 anti_patterns.py: prompting mistakes that cost real money
# Prompt anti-patterns and their fixes
anti_patterns = {
"Prompt stuffing": {
"bad": "Please please please make sure to really definitely absolutely "
"without any doubt whatsoever generate a JSON response...",
"good": "Respond with valid JSON only. No additional text.",
"why": "Repetition wastes tokens and does not improve compliance. "
"Clear, short constraints work better than begging.",
},
"Negative instructions": {
"bad": "Don't use technical jargon. Don't write long paragraphs. "
"Don't include examples. Don't use bullet points.",
"good": "Write in plain English. Use short sentences (max 15 words). "
"Explain concepts with analogies, not code.",
"why": "LLMs follow positive instructions better than negative ones. "
"'Don't do X' often makes the model think about X more.",
},
"Ambiguous output format": {
"bad": "Give me information about Python lists.",
"good": "List 5 Python list methods. For each: method name, one-line "
"description, return type. Format as a markdown table.",
"why": "Without format constraints, the model guesses what you want. "
"Explicit format = consistent output = parseable by code.",
},
"Context overload": {
"bad": "Here is my entire 50,000-word codebase. Find the bug.",
"good": "Here is the function that fails (20 lines). Here is the error "
"message. Here is the expected vs actual output.",
"why": "More context is not always better. Models lose accuracy when "
"relevant info is buried in irrelevant context.",
},
}
print("Prompt Engineering Anti-Patterns\n")
for name, info in anti_patterns.items():
print(f" Anti-pattern: {name}")
print(f" Bad: {info['bad'][:80]}...")
print(f" Good: {info['good'][:80]}...")
print(f" Why: {info['why'][:80]}...")
print()
▶ Output (verified on Python 3.14.6)
Prompt Engineering Anti-Patterns
Anti-pattern: Prompt stuffing
Bad: Please please please make sure to really definitely absolutely without any doubt...
Good: Respond with valid JSON only. No additional text....
Why: Repetition wastes tokens and does not improve compliance. Clear, short constrain...
Anti-pattern: Negative instructions
Bad: Don't use technical jargon. Don't write long paragraphs. Don't include examples....
Good: Write in plain English. Use short sentences (max 15 words). Explain concepts wit...
Why: LLMs follow positive instructions better than negative ones. 'Don't do X' often ...
Anti-pattern: Ambiguous output format
Bad: Give me information about Python lists....
Good: List 5 Python list methods. For each: method name, one-line description, return ...
Why: Without format constraints, the model guesses what you want. Explicit format = c...
Anti-pattern: Context overload
Bad: Here is my entire 50,000-word codebase. Find the bug....
Good: Here is the function that fails (20 lines). Here is the error message. Here is t...
Why: More context is not always better. Models lose accuracy when relevant info is bu...
Common Python Prompt Engineering Mistakes
- Not testing prompts systematically: One good response does not mean the prompt works. Test it against 20 or more varied inputs and measure how consistent the output is.
- Hardcoding model names: Put the model name in one constant (like the
MODELvariable in the examples above). Models ship fast, so when the next one lands you flip one line instead of hunting through the codebase. - Ignoring temperature: Use 0 for factual extraction, 0.3 to 0.5 for structured generation, and 0.7 to 1.0 for creative tasks. The default is wrong for most production work, so set it on purpose.
Interview Corner
Q: What is the difference between few-shot prompting and fine-tuning?
Few-shot prompting puts examples in the prompt at inference time, so the model’s weights never change. Fine-tuning (fine-tuning tutorial) actually updates the weights from training data, giving you a permanently changed model. Few-shot is faster to iterate (edit the prompt, get new behavior instantly) but you pay for those example tokens on every single request. Fine-tuning costs more upfront but is cheaper per request once deployed, and it can capture patterns too messy to teach with a handful of examples. Rule of thumb: reach for few-shot first, and only fine-tune when prompting plateaus.
Q: Does “think step by step” still help on modern reasoning models?
Less than it used to. On older models a bare “let’s think step by step” gave a big jump, because it forced reasoning the model would otherwise skip. Many current frontier models already reason internally before answering, so that exact phrase adds little. What still helps is structure: telling the model the specific steps you want, asking it to show intermediate state, and requesting the final answer in a fixed format you can parse. So the spirit of chain-of-thought lives on even if the magic phrase has faded.
Practice Exercises
- Exercise 1 (few-shot): Take the entity-extraction prompt and add a fourth example that returns null for both
plan_mentionedandemailwhen neither appears. Then test it on a message that is missing both and confirm the model copies the pattern. - Exercise 2 (templates): Add a third template to the
PromptTemplateregistry, aTRANSLATEtemplate with slots for${source_lang},${target_lang}, and${text}. Render it and print the character counts, the same way the registry loop does. - Exercise 3 (self-consistency): Change
n_samplesfrom 3 to 5 and feed in a question where the model sometimes slips, like a multi-step word problem. Watch how often the majority vote rescues the right answer compared to a single call.
More in this series:
- GenAI: Introduction to Large Language Models
- LLM Tool Calling: Build a Raw Agent Loop From Scratch
- Python Chatbot Project: Multi-Provider Chat App with Costs
Frequently Asked Questions
What is prompt engineering in Python?
Prompt engineering in Python is the practice of writing structured instructions for a large language model and calling it through an SDK such as openai or anthropic. The same model gives a vague or a sharp answer depending on how the prompt specifies the task, context, examples, and output format. Good Python prompt engineering keeps prompts as reusable, versioned templates rather than ad-hoc strings.
What temperature should I use?
Use temperature 0 for deterministic tasks (extraction, classification, code). Use 0.3 to 0.5 for structured generation (summaries, analysis). Use 0.7 to 1.0 for creative tasks (brainstorming, writing). Lower temperature means more predictable output, higher means more creative but less reliable.
How do I reduce token costs?
Shorten system prompts by testing what you can remove without losing quality. Cache responses for identical inputs. Send simple tasks to a cheaper, smaller model and reserve the larger flagship models for genuinely hard work. Batch similar requests. Set max_tokens to the minimum you actually need. Model names change often, so check the provider docs for the current low-cost option.
How do I get reliable JSON output?
Use the structured output mode your SDK provides (for example response_format with a json_object type in OpenAI). Include a JSON example in your prompt and set temperature to 0. For complex schemas, use function calling (LLM API and function calling tutorial), which enforces schema compliance. Always validate the result with Pydantic before you trust it.
Is there an optimal prompt length?
Shorter prompts are cheaper but less precise. Prompts with specific constraints (format, length, style) tend to beat both very short and very long prompts. A practical sweet spot is roughly 100 to 300 words for the system prompt and as few tokens as possible for the user message. Always measure: track output quality against prompt length.
Interview Questions on Prompt Engineering
The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.
Q: When would you choose zero-shot prompting over few-shot, and what does few-shot buy you?
Reach for zero-shot when the task is simple and self-explanatory, like classifying sentiment or extracting a date, and a clear instruction with an explicit output format is enough. Move to few-shot when the model keeps missing your exact format, label set, or edge-case handling, because two to five worked examples pin down the pattern that words alone cannot. Few-shot costs you extra input tokens on every request, so the trade is precision against price. A good rule is to start zero-shot and add examples only where output drifts.
Q: What is a system prompt, and how is it different from a user prompt?
A system prompt sets the role, constraints, and output format once, up front, and it shapes every reply for the whole conversation. A user prompt is the specific request that comes after, and it can change on every turn. Keeping stable rules (the persona, the guardrails, the format) in the system prompt means you do not repeat them in each user message, and the model treats them as higher-priority context. Think of the system prompt as the job description and the user prompt as the individual task tickets.
Q: What is self-consistency and when is it worth the extra cost?
Self-consistency runs the same chain-of-thought prompt several times at a higher temperature so each run reasons a little differently, then takes the majority answer. It raises accuracy on problems that have one correct answer but where a single run sometimes slips, like multi-step math or logic. The cost is linear in the number of samples, so three to five calls per question, which makes it worth it for high-stakes or error-prone tasks and wasteful for easy ones. It does nothing for open-ended creative work where there is no single right answer to vote on.
Q: What is the ReAct pattern and why does it matter for agents?
ReAct (Reasoning and Acting) interleaves a Thought, an Action, and an Observation in a loop: the model reasons about what it needs, takes an action such as calling a tool, reads the result, and repeats until it can answer. This matters for agents because it lets the model use real tools (search, a calculator, an API) instead of guessing from memory, and each observation grounds the next step in actual data. It is the backbone of most tool-using agent frameworks.
Q: Your few-shot classifier passed every test case but returns inconsistent labels in production. What do you check first?
Start with temperature: if it is not 0, set it to 0 for a classification task so the output stops varying run to run. Next, check whether production inputs look like your examples; real messages are often longer, messier, or in a different tone than the clean examples you tested, so the pattern generalizes worse. Confirm your examples actually cover the ambiguous label boundaries, and add an example for the class that is being confused. Finally, make sure you constrained the output to the exact allowed label set, so the model cannot invent a new category.
Q: An LLM feature’s cost tripled overnight with no change in traffic. Where do you look?
First check whether the model constant changed, since swapping a small model for a larger flagship one multiplies the per-token price. Then inspect token usage per request: a longer system prompt, growing few-shot examples, or unbounded conversation history that you keep appending will inflate input tokens on every call. Look at max_tokens and whether outputs got longer, and whether retries or a self-consistency loop are firing more often than expected. Log response.usage per call so you can pinpoint which prompt or code path drove the spike instead of guessing.
What’s Next?
You now know how to talk to LLMs so they actually listen, which is the core skill Python prompt engineering builds on. Next up, the LLM API and function calling tutorial covers the nuts and bolts of calling these models from Python: authentication, streaming responses, structured output with Pydantic, and function calling that lets an LLM trigger real tools in your code. You walked away from this post able to pick the right prompting pattern for a task, cut hallucinations, get structured output you can parse, and manage prompts as versioned templates instead of scattered strings.
Want the full roadmap from Python basics to shipping AI features? Browse the complete Python + AI/ML tutorial series home to see where this fits and what to read next.
Further reading: for the full reference, see Hugging Face documentation.
Related Posts
Previous: How to Choose an LLM: Open vs Closed, Size, Cost, Licenses
Next: GenAI: LLM APIs (OpenAI, Anthropic, Structured Output & Function Calling)
Series Home: Python + AI/ML Tutorial Series

No comment