Foundations of Prompt Engineering¶
Prerequisites¶
Before this lesson you should be comfortable with:
- LLM basics — what a language model is and how chat APIs work (Modules 01–02)
- Python fundamentals — enough to read and run the code examples below
- Basic API usage — calling
client.chat.completions.create()with messages
You do not need prior prompt engineering experience. This lesson establishes the vocabulary and patterns used throughout the module.
What You'll Learn¶
| Objective | Time | Difficulty |
|---|---|---|
| Explain what prompt engineering is and why it matters in production | 10 min | Intermediate |
| Decompose any prompt into six structural components | 10 min | Intermediate |
| Apply clarity, specificity, and structure principles to real tasks | 10 min | Intermediate |
| Run a minimal prompt comparison in Python | 10 min | Intermediate |
Intuition First: The Briefing Room Analogy¶
Imagine you walk into a briefing room and tell a brilliant analyst: "Tell me about Python." They might talk about the snake, the programming language, or Monty Python — you gave them no constraints.
Now imagine you say: "You are a senior data engineer briefing a Java developer who is evaluating Python for a new ETL pipeline. Give me three concrete advantages of Python for data science, each with a five-line code example. Keep each advantage under 80 words."
Same analyst, radically different output. The second briefing specifies who they are, who the audience is, what to deliver, and how to format it.
That is prompt engineering. You are not programming the model's weights — you are programming its context window. Every token you add shifts the probability distribution over what comes next. Good prompts narrow that distribution to the output you actually need.
What Is Prompt Engineering?¶
Prompt engineering is the practice of designing inputs to language models that reliably produce high-quality, accurate outputs. It sits at the intersection of communication, programming, and understanding how LLMs process language.
Unlike traditional software where logic is explicit, LLM behavior is probabilistic. The same model can produce wildly different outputs depending on how you ask:
from openai import OpenAI
client = OpenAI()
# Vague prompt — unpredictable output
vague = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Tell me about Python"}],
)
print(vague.choices[0].message.content[:200])
# Could be about the snake, the programming language, or Monty Python
# Specific prompt — targeted output
specific = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": (
"Explain 3 key advantages of Python for data science, "
"with a brief code example for each. Target audience: "
"developers switching from Java."
),
}],
)
print(specific.choices[0].message.content[:200])
# Focused, actionable, audience-appropriate response
In production, prompt quality is often the difference between a feature that ships and one that gets rolled back. A 200-token prompt change can move classification accuracy from 72% to 91% without touching model weights.
The Anatomy of a Great Prompt¶
Every effective prompt contains some or all of these six components:
┌─────────────────────────────────────────────┐
│ 1. ROLE / PERSONA │
│ Who should the model act as? │
│ │
│ 2. CONTEXT │
│ Background information needed │
│ │
│ 3. TASK / INSTRUCTION │
│ What exactly should the model do? │
│ │
│ 4. FORMAT / STRUCTURE │
│ How should the output look? │
│ │
│ 5. CONSTRAINTS │
│ Boundaries, limits, things to avoid │
│ │
│ 6. EXAMPLES (optional) │
│ Show what good output looks like │
└─────────────────────────────────────────────┘
All Components Together¶
def build_code_review_prompt(code: str) -> str:
return f"""You are a senior Python developer who specializes in code review.
Context:
I have a Flask API endpoint that handles user registration.
The code works but I'm concerned about security and best practices.
Task:
Review this code and provide:
1. Security vulnerabilities found
2. Performance improvements
3. A refactored version with fixes applied
Format:
- Use markdown headers for each section
- Include severity ratings (Critical/High/Medium/Low)
- Add inline code comments in the refactored version
Constraints:
- Keep the same API contract (don't change endpoint paths)
- Use only standard library + Flask (no new dependencies)
- Assume Python 3.11+
Here is the code:
{code}
"""
# Usage
prompt = build_code_review_prompt("""
@app.route('/register', methods=['POST'])
def register():
data = request.json
query = f"INSERT INTO users (email) VALUES ('{data['email']}')"
db.execute(query)
return {"status": "ok"}
""")
When a prompt fails in production, the fix usually maps to one of these six slots. "The model is too verbose" → tighten constraints. "It ignores my format" → strengthen format or add examples. Diagnose by component, not by guessing.
Core Principle 1: Be Specific¶
The biggest mistake in prompt engineering is being too vague. Specificity beats brevity.
| Bad Prompt | Good Prompt |
|---|---|
| "Summarize this" | "Summarize this article in 3 bullet points, each under 20 words, focusing on business impact" |
| "Write code" | "Write a Python function that validates email addresses using regex, returns True/False, and handles edge cases like '+' aliases" |
| "Explain AI" | "Explain how transformer attention mechanisms work to a CS undergrad who understands matrix multiplication" |
Specificity reduces the model's search space. Instead of generating from all possible "summaries," it generates from summaries that are exactly three bullets, each under 20 words, focused on business impact.
Core Principle 2: Provide Structure¶
LLMs follow structure remarkably well. Use XML tags, markdown headers, or numbered lists to separate concerns:
debug_prompt = """
<context>
You are helping a user debug a React application.
The app uses Next.js 15 with the App Router.
</context>
<error_message>
TypeError: Cannot read properties of undefined (reading 'map')
at ProductList (./components/ProductList.tsx:12:24)
</error_message>
<task>
1. Identify the root cause of this error
2. Explain why it happens in simple terms
3. Provide the fix with before/after code
</task>
<format>
Use markdown. Start with a one-line diagnosis, then details.
</format>
"""
Structure also makes prompts testable. You can assert that rendered templates contain required sections before sending them to the API.
Core Principle 3: Use the System Message¶
Most chat APIs support a system message that sets persistent behavior across the conversation:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"You are a database expert. Always suggest the most "
"performant query. When writing SQL, include EXPLAIN "
"ANALYZE output estimates. If the user's approach has "
"a better alternative, mention it proactively."
),
},
{
"role": "user",
"content": (
"How do I find duplicate emails in a users table "
"with 10M rows?"
),
},
],
)
System Message Best Practices¶
| Do | Don't |
|---|---|
| Define the role and expertise | Leave system message empty |
| Set output format preferences | Put conversation history in system |
| Specify constraints and boundaries | Make it extremely long (wastes tokens) |
| Include domain-specific instructions | Contradict the system message in user messages |
The system message is your persistent contract with the model. User messages carry the variable data; the system message carries the rules that should not change turn to turn.
Core Principle 4: Iterate and Refine¶
Prompt engineering is iterative. Start simple, measure output quality, then refine.
Iteration 1: "Translate this to Spanish"
Problem: Too formal for a casual chat app
Iteration 2: "Translate this to casual Latin American Spanish"
Problem: Sometimes uses region-specific slang
Iteration 3: "Translate this to casual Spanish suitable for a
general Latin American audience. Avoid country-specific slang.
Keep the tone friendly and conversational."
Result: Consistent, appropriate translations
Treat prompts like code: version them, test them, and review failures systematically. Later lessons cover eval sets and A/B testing; the mindset starts here.
Runnable Example: Prompt Builder¶
from dataclasses import dataclass
from openai import OpenAI
client = OpenAI()
@dataclass
class PromptParts:
role: str
context: str
task: str
format_spec: str
constraints: str
def render_prompt(parts: PromptParts) -> str:
return f"""Role: {parts.role}
Context: {parts.context}
Task: {parts.task}
Output Format: {parts.format_spec}
Constraints: {parts.constraints}
"""
def call_llm(prompt: str, system: str = "") -> str:
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.2,
)
return response.choices[0].message.content
# Build a structured PR review prompt
review = PromptParts(
role="Senior Node.js developer and security expert",
context="Reviewing a PR for an authentication microservice using Express.js",
task="Identify bugs, security issues, and suggest improvements",
format_spec="Markdown table with columns: Issue, Severity, Line, Fix",
constraints="Focus on OWASP Top 10 vulnerabilities. Max 10 findings.",
)
prompt = render_prompt(review)
# print(call_llm(prompt)) # Uncomment with API key configured
Production Connection¶
In production, prompt anatomy directly affects reliability and cost:
- Version every prompt — store prompts as named artifacts (
summarize_v3), not inline strings. When output quality shifts, you need to know which prompt version was live. - Log prompt hash + version with every LLM call so you can correlate failures to prompt changes.
- A/B test structural changes — adding a constraint section vs. adding examples often produces different accuracy/cost tradeoffs. Measure both.
- Failure recovery — when output violates format constraints, retry with a stricter format instruction before falling back to a simpler model or default response.
Teams that treat prompts as first-class configuration — with the same rigor as database schemas — ship faster and debug less.
Edge Cases & Common Misconceptions¶
Misconception 1: Longer prompts are always better. Extra tokens cost money and can dilute key instructions. A focused 150-token prompt often beats a rambling 800-token one. Put the most important constraints first.
Misconception 2: The model "understands" implied requirements. If you need JSON, say JSON. If you need three bullets, say three bullets. Implicit expectations become production bugs.
Misconception 3: Prompt engineering replaces eval. Good structure helps, but you still need test cases. A prompt that works on five examples may fail on edge cases you never considered.
Misconception 4: One prompt fits all models. The anatomy stays the same, but optimal phrasing varies by provider. Lesson 9 covers model-specific adaptations.
Key Takeaways¶
- Prompt engineering programs the context window, not the model weights — specificity narrows the output distribution.
- Every effective prompt has up to six components: role, context, task, format, constraints, and optional examples.
- Specificity beats brevity; vague prompts produce vague (or wrong) outputs.
- Structure (XML tags, headers, numbered lists) makes prompts easier for models to follow and for engineers to debug.
- Use the system message for persistent rules; use the user message for variable task data.
- Prompt engineering is iterative — start simple, identify failure patterns, refine by component.
- In production, version prompts, log which version ran, and retry with stricter instructions on format failures.
- Diagnose prompt failures by asking which anatomy component is missing or weak.
Next Lesson¶
Lesson 2: Few-Shot and Chain-of-Thought Prompting — Learn how examples and step-by-step reasoning dramatically improve model output on classification and logic tasks.