Module 2: Zero-Shot and Few-Shot Prompting
5. Output Formatting and Parsing
Capsule overview
In production, the LLM's output has to be parseable by code. An output that's "almost JSON" or that comes wrapped in extra text breaks your pipeline. In this capsule you'll learn the four main formats (JSON, XML, Markdown, CSV), techniques to maximize format consistency, robust extraction with regex when the output drifts, and how to handle malformed outputs with retry logic.
The focus is production: you need your code to work 99.9% of the time, not 80%. For that, the parsing system has to handle every variation LLMs can generate, from pure JSON to JSON wrapped in markdown with an introductory sentence.
Why it matters: If your classifier returns "TECHNICAL" 95% of the time but "The ticket is of type TECHNICAL." the other 5%, you have a production bug that shows up intermittently — the worst kind of bug. This capsule gives you the tools to eliminate it.
The 4 Main Formats
JSON — The standard for APIs
JSON is the most used format when the output is going to be processed by code. Easy to parse, extensible, and familiar to every LLM.
from openai import OpenAI
import json
client = OpenAI()
# Basic extraction with JSON
def extract_entities(text: str) -> dict:
"""
Extracts people, organizations and locations from the text.
Uses JSON mode to guarantee valid JSON.
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """
Extract entities from the text.
Response format (exact JSON):
{"people": ["name1", ...], "organizations": ["org1", ...], "locations": ["location1", ...]}
If there are no entities of some type, use an empty list. Only JSON, nothing else.
"""
},
{"role": "user", "content": text}
],
temperature=0,
response_format={"type": "json_object"} # Guarantees valid JSON (OpenAI)
)
return json.loads(response.choices[0].message.content)
# Test
texts = [
"Maria Garcia from Google visited the Microsoft office in Madrid.",
"Apple's CEO met with the president of France in Paris.",
"There are no people or places mentioned in this text."
]
for t in texts:
result = extract_entities(t)
print(f"Input: {t[:60]}")
print(f"Output: {result}\n")
Output:
Input: Maria Garcia from Google visited the Microsoft office in Mad
Output: {'people': ['Maria Garcia'], 'organizations': ['Google', 'Microsoft'], 'locations': ['Madrid']}
Input: Apple's CEO met with the president of France in Paris.
Output: {'people': ["Apple's CEO", 'president of France'], 'organizations': ['Apple'], 'locations': ['Paris']}
Input: There are no people or places mentioned in this text.
Output: {'people': [], 'organizations': [], 'locations': []}
XML — For nested structure and Claude
XML is especially effective with Claude, which is optimized to follow instructions with tags. It's also useful for documents with a hierarchical structure.
from anthropic import Anthropic
import xml.etree.ElementTree as ET
ant_client = Anthropic()
def extract_xml_analysis(text: str) -> dict:
"""Extracts a structured analysis in XML format (optimal for Claude)."""
response = ant_client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=500,
system="""
Analyze the text and respond ONLY with XML in this exact format:
<analysis>
<sentiment>POSITIVE|NEGATIVE|NEUTRAL</sentiment>
<topics>
<topic>topic1</topic>
<topic>topic2</topic>
</topics>
<summary>one-sentence summary</summary>
</analysis>
No text before or after the XML.
""",
messages=[{"role": "user", "content": text}]
)
raw_xml = response.content[0].text.strip()
# Parse XML
root = ET.fromstring(raw_xml)
return {
"sentiment": root.find("sentiment").text,
"topics": [t.text for t in root.findall("topics/topic")],
"summary": root.find("summary").text
}
# Test
text = "The new iPhone has an incredible camera but the battery barely lasts. Fans are divided."
try:
result = extract_xml_analysis(text)
print(f"Analysis: {result}")
except ET.ParseError as e:
print(f"Error parsing XML: {e}")
Markdown — For human-readable output
def generate_markdown_report(data: dict) -> str:
"""Generates a Markdown report for display."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """
Generate an executive report in Markdown with exactly this structure:
## Executive Summary
[2-3 sentences]
## Key Metrics
| Metric | Value | Trend |
|--------|-------|-------|
[data rows]
## Recommendations
1. [First recommendation]
2. [Second recommendation]
3. [Third recommendation]
Markdown only. No additional text.
"""
},
{"role": "user", "content": str(data)}
],
temperature=0.2
)
return response.choices[0].message.content
# Test
sales_data = {
"Q1": 1200000, "Q2": 1500000, "Q3": 1100000, "Q4": 1800000,
"top_product": "Enterprise Plan", "retention_rate": "87%"
}
print(generate_markdown_report(sales_data))
CSV — For tabular data
import csv
import io
def extract_csv_data(text: str) -> list[dict]:
"""Extracts structured data in CSV format."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """
Extract the products mentioned in the text.
Exact CSV format (with header):
name,price,available
Rules:
- One row per product
- price: number only, no currency symbol (e.g. 29.99)
- available: true or false
- If there is no price: leave it empty (two commas in a row)
CSV only. No additional text.
"""
},
{"role": "user", "content": text}
],
temperature=0
)
raw_csv = response.choices[0].message.content.strip()
reader = csv.DictReader(io.StringIO(raw_csv))
return list(reader)
products_text = "We have the Pro laptop for $1299 (in stock) and the basic mouse for $25 (sold out)."
products = extract_csv_data(products_text)
print(f"Extracted products: {products}")
Techniques to Guarantee Format Consistency
Technique 1: Prominent explicit instruction
The format instruction has to be impossible to ignore:
# ❌ Buried instruction
prompt = """
You are a data extractor. Analyze the text and find the relevant data.
Make sure you're precise. The response format must be JSON with the keys: name, email.
The text may contain several types of information. Only extract name and email.
Text: [...]
"""
# ✅ Prominent instruction, at the end
prompt = """
Extract name and email from the text.
REQUIRED FORMAT (only this, nothing else):
{"name": "string or null", "email": "string or null"}
Text: [...]
"""
Technique 2: A concrete output example
def extract_contact(text: str) -> dict:
"""Extracts a contact with an explicit output example."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """
Extract name and email from the text.
Example of a correct output: {"name": "John Garcia", "email": "john@mail.com"}
Example if the email is missing: {"name": "Anna Perez", "email": null}
Example if there's nothing: {"name": null, "email": null}
Respond with the JSON ONLY. No text before or after.
"""
},
{"role": "user", "content": text}
],
temperature=0,
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
# Test with edge cases
cases = [
"Contact Michael at michael@company.com",
"There's only a name here: Robert",
"This message has no contact data"
]
for c in cases:
print(f"'{c}' → {extract_contact(c)}")
Technique 3: JSON Mode (OpenAI) and JSON Schema
OpenAI offers two levels of guarantee for JSON:
# Level 1: JSON Mode — guarantees valid JSON, no fixed schema
response_json_mode = client.chat.completions.create(
model="gpt-4o-mini",
messages=[...],
response_format={"type": "json_object"}, # Valid JSON guaranteed
temperature=0
)
# Level 2: Structured Outputs — JSON that conforms to a specific JSON Schema
from pydantic import BaseModel
class ContactOutput(BaseModel):
name: str | None
email: str | None
# With OpenAI's parse() (requires pydantic)
response_structured = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[...],
response_format=ContactOutput # Guaranteed schema, validated by Pydantic
)
contact = response_structured.choices[0].message.parsed
print(f"name={contact.name}, email={contact.email}")
Technique 4: Delimiter tags
Use tags to mark exactly where the output starts and ends:
SYSTEM = """
Classify the sentiment of the text.
Respond with the category between tags:
<sentiment>POSITIVE|NEGATIVE|NEUTRAL</sentiment>
That's all. No text before or after the tags.
"""
def extract_from_tags(raw: str, tag: str) -> str | None:
"""Extracts the content between XML tags."""
import re
match = re.search(f'<{tag}>(.*?)</{tag}>', raw, re.DOTALL)
return match.group(1).strip() if match else None
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": "The product is incredible, I recommend it."}
],
temperature=0
)
sentiment = extract_from_tags(response.choices[0].message.content, "sentiment")
print(f"Sentiment: {sentiment}") # POSITIVE
Robust Parsing: Handling Malformed Outputs
LLMs can return JSON wrapped in markdown, with text before or after, or with small syntax errors. Your parser has to handle all of it:
import re
import json
def robust_json_parse(raw: str) -> dict | None:
"""
Multi-strategy JSON parser.
Handles: pure JSON, ```json...```, text+JSON, trailing commas.
Returns None if it can't parse.
"""
raw = raw.strip()
# Strategy 1: pure JSON
try:
return json.loads(raw)
except json.JSONDecodeError:
pass
# Strategy 2: extract from a markdown block ```json...``` or ```...```
match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', raw, re.DOTALL)
if match:
try:
return json.loads(match.group(1).strip())
except json.JSONDecodeError:
pass
# Strategy 3: find the first balanced { ... }
start = raw.find('{')
if start == -1:
return None
depth = 0
for i, char in enumerate(raw[start:], start):
if char == '{':
depth += 1
elif char == '}':
depth -= 1
if depth == 0:
candidate = raw[start:i+1]
# Clean trailing commas (a common JSON error)
candidate = re.sub(r',\s*([}\]])', r'\1', candidate)
try:
return json.loads(candidate)
except json.JSONDecodeError:
break
return None
# Test with real problematic outputs
problematic_outputs = [
'{"name": "John", "email": "john@mail.com"}', # Perfect
'```json\n{"name": "Anna"}\n```', # Markdown
'Here is the result:\n{"name": "Peter"}', # With text
'{"name": "Charles",}', # Trailing comma
'```\n{"name": "Louis"}\n```', # No "json" label
]
for output in problematic_outputs:
result = robust_json_parse(output)
status = "✅" if result else "❌"
print(f"{status} '{output[:50]}' → {result}")
Output:
✅ '{"name": "John", "email": "john@mail.com"}' → {'name': 'John', 'email': 'john@mail.com'}
✅ '```json\n{"name": "Anna"}\n```' → {'name': 'Anna'}
✅ 'Here is the result:\n{"name": "Peter"}' → {'name': 'Peter'}
✅ '{"name": "Charles",}' → {'name': 'Charles'}
✅ '```\n{"name": "Louis"}\n```' → {'name': 'Louis'}
Retry Logic with Feedback
When the first attempt fails, the retry includes specific feedback about what went wrong:
def parse_with_retry(
raw: str,
expected_schema: str,
max_retries: int = 2
) -> dict:
"""
Tries to parse the output, retrying with feedback if it fails.
Args:
raw: The model's output
expected_schema: Description of the expected format, for the feedback
max_retries: Number of additional attempts
"""
# First attempt: direct parsing
result = robust_json_parse(raw)
if result is not None:
return result
# Retry with feedback
for attempt in range(max_retries):
print(f" Retry {attempt + 1}: output was not valid JSON")
fix_prompt = f"""
The previous output was not valid JSON.
The output I received:
{raw[:500]}
I need exactly this format:
{expected_schema}
Respond ONLY with valid JSON. No additional text.
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": fix_prompt}],
temperature=0,
response_format={"type": "json_object"} # Forces JSON on the retry
)
raw = response.choices[0].message.content
result = robust_json_parse(raw)
if result is not None:
print(f" ✅ Retry {attempt + 1} succeeded")
return result
raise ValueError(f"Could not parse after {max_retries} retries. Last output: {raw[:200]}")
# Usage
SCHEMA = '{"sentiment": "POSITIVE|NEGATIVE|NEUTRAL", "confidence": 0.0-1.0}'
# Simulate an initial malformed output
initial_raw = "The sentiment is positive with high confidence." # Not JSON
try:
result = parse_with_retry(initial_raw, SCHEMA)
print(f"Final result: {result}")
except ValueError as e:
print(f"Error: {e}")
Validation with Pydantic
After parsing the JSON, validate that it matches the expected schema:
from pydantic import BaseModel, Field, ValidationError
from typing import Literal
class SentimentAnalysis(BaseModel):
sentiment: Literal["POSITIVE", "NEGATIVE", "NEUTRAL"]
confidence: float = Field(ge=0.0, le=1.0)
main_aspect: str | None = None
def classify_with_validation(text: str) -> SentimentAnalysis:
"""Classifies and validates with Pydantic."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": """
Analyze the sentiment.
JSON: {"sentiment": "POSITIVE|NEGATIVE|NEUTRAL", "confidence": 0.0-1.0, "main_aspect": "string or null"}
JSON only.
"""
},
{"role": "user", "content": text}
],
temperature=0,
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
try:
return SentimentAnalysis(**data)
except ValidationError as e:
# Sanitize out-of-range values before failing
if "confidence" in data:
data["confidence"] = max(0.0, min(1.0, float(data.get("confidence", 0.5))))
return SentimentAnalysis(**data)
# Test
texts = [
"The product is excellent, highly recommended",
"Terrible experience, never again",
"Fine, neither good nor bad"
]
for t in texts:
result = classify_with_validation(t)
print(f"'{t[:40]}' → {result.model_dump()}")
Format Comparison
| Format | Parseable by code | Human-readable | Relative size | When to use |
|---|---|---|---|---|
| JSON | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Medium | APIs, structured data, pipeline |
| JSON Schema | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Medium | Production, strict validation |
| XML | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Verbose | Claude, hierarchical documents |
| Markdown | ⭐⭐ | ⭐⭐⭐⭐⭐ | Variable | Human reports, display |
| CSV | ⭐⭐⭐⭐ | ⭐⭐ | Very compact | Simple tables, export |
| Exact string | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Minimal | Classification, yes/no, one word |
Connection to the Project
In the Few-Shot Classification System (capsule 08) the classification output always goes through:
robust_json_parse()to pull the JSON out of the LLM's outputSentimentAnalysis(or an equivalent schema) to validate with Pydanticparse_with_retry()if the first attempt fails- Sanitization of out-of-range values before failing with an error
This pipeline guarantees the system doesn't break on unexpected outputs.
Troubleshooting
Problem 1: JSON with a trailing comma
Cause: The model generates {"a": 1, "b": 2,} — that's not standard JSON.
Fix:
# Clean it before json.loads
raw_clean = re.sub(r',\s*([}\]])', r'\1', raw)
json.loads(raw_clean)
Problem 2: Truncated output
Cause: max_tokens too low for the complete JSON.
Fix:
# Estimate the tokens you need: 1 token ≈ 4 chars of JSON
# If the expected output is ~500 chars, set max_tokens=150+
max_tokens = len(json.dumps(expected_schema)) // 3 # With buffer
Problem 3: Strings with unescaped quotes
Cause: "text with "quotes" inside" — invalid JSON.
Fix: Use JSON mode in OpenAI (it guarantees valid JSON) or Structured Outputs to avoid the problem entirely. If you already have the output, a retry with feedback usually fixes it.
Problem 4: The model adds "Sure, here's the JSON:" first
Cause: The format instruction isn't strict enough.
Fix:
# Add to the system prompt:
"Your response must start DIRECTLY with { and end with }."
"No greetings, no explanations, no text before or after the JSON."
Problem 5: Malformed XML (unclosed tags)
Cause: The model generates <sentiment>POSITIVE with no closing tag.
Fix: Use BeautifulSoup with a lenient parser for irregular XML:
from bs4 import BeautifulSoup
soup = BeautifulSoup(raw_xml, "xml") # More tolerant parser than ElementTree
sentiment = soup.find("sentiment").text
Exercises
Exercise 1: Multi-format parser (Easy)
Implement a function that automatically detects whether the output is JSON, XML, or a plain string, and parses it accordingly.
See solution
def auto_parse(raw: str) -> dict | str:
"""
Detects the format and parses automatically.
Returns: dict (for JSON/XML) or str (for plain text)
"""
raw = raw.strip()
# Try JSON
json_result = robust_json_parse(raw)
if json_result:
return json_result
# Try XML
if raw.startswith("<") or "</" in raw:
try:
root = ET.fromstring(raw)
# Convert simple XML to a dict
return {child.tag: child.text for child in root}
except ET.ParseError:
pass
# Plain text
return raw
# Test
outputs = [
'{"key": "value"}',
'<result><value>42</value></result>',
'POSITIVE'
]
for o in outputs:
print(f"'{o}' → {auto_parse(o)} (type: {type(auto_parse(o)).__name__})")
Exercise 2: Validate a complex schema (Medium)
Create a Pydantic schema for the Prompt Analyzer's output (with classification, components, suggestions, quality) and a function that parses and validates it.
See solution
from pydantic import BaseModel, Field
from typing import Literal
class ClassificationOutput(BaseModel):
technique: Literal["zero-shot", "few-shot", "chain-of-thought", "mixed"]
confidence: float = Field(ge=0.0, le=1.0)
class ComponentsOutput(BaseModel):
instruction: Literal["present", "absent", "implicit"]
context: Literal["present", "absent", "implicit"]
output_format: Literal["present", "absent", "implicit"]
class PromptAnalysisOutput(BaseModel):
classification: ClassificationOutput
components: ComponentsOutput
suggestions: list[str] = Field(min_length=1, max_length=6)
score: int = Field(ge=0, le=100)
def parse_prompt_analysis(raw: str) -> PromptAnalysisOutput | None:
data = robust_json_parse(raw)
if not data:
return None
try:
return PromptAnalysisOutput(**data)
except ValidationError as e:
print(f"Validation failed: {e}")
return None
Exercise 3: Retry with specific feedback (Hard)
Implement an improved version of parse_with_retry where the feedback message states exactly which part of the schema failed (missing field, wrong type, out-of-range value).
See solution
def parse_with_detailed_retry(
raw: str,
schema_cls: type[BaseModel],
max_retries: int = 2
) -> BaseModel:
"""Retry with detailed feedback about the validation error."""
for attempt in range(max_retries + 1):
data = robust_json_parse(raw)
if data:
try:
return schema_cls(**data)
except ValidationError as e:
# Pull the specific errors out of Pydantic
errors = []
for error in e.errors():
field = ".".join(str(l) for l in error["loc"])
error_type = error["type"]
message = error["msg"]
errors.append(f" - Field '{field}': {error_type} — {message}")
error_detail = "\n".join(errors)
if attempt < max_retries:
fix_prompt = f"""
The previous JSON has validation errors:
{error_detail}
The invalid JSON I received:
{raw[:300]}
Expected schema: {schema_cls.model_json_schema()}
Fix it and return ONLY the valid JSON.
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": fix_prompt}],
temperature=0,
response_format={"type": "json_object"}
)
raw = response.choices[0].message.content
else:
if attempt < max_retries:
fix_prompt = f"The output is not valid JSON: '{raw[:100]}'. Return JSON only."
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": fix_prompt}],
temperature=0,
response_format={"type": "json_object"}
)
raw = response.choices[0].message.content
raise ValueError(f"Parsing failed after {max_retries} retries")
Summary
In this capsule you learned:
- Formats: JSON for APIs/code, XML for Claude/hierarchy, Markdown for humans, CSV for tables, exact string for classification
- Consistency techniques: Prominent instruction at the end, output example, JSON mode (OpenAI), Structured Outputs, XML delimiter tags
- Robust parser: Multi-strategy: pure JSON → markdown → first
{...}→ clean trailing commas - Retry logic: Direct first attempt, retry with specific feedback if it fails, JSON mode forced on the retry
- Pydantic validation: Parse + validate the schema + sanitize out-of-range values + specific error feedback
Next capsule: Boundary testing — what happens when you get empty, adversarial, or extremely long inputs, and how to make your prompts defensive.
Further resources
- OpenAI JSON Mode — Documentation for
response_formatand Structured Outputs with JSON Schema - OpenAI Structured Outputs (Pydantic) — How to use
client.beta.chat.completions.parse()with Pydantic models - Pydantic v2 Validators — Advanced validators for complex schemas
- Anthropic Structured Outputs — Anthropic's native JSON Schema (beta)
- Python json module — Complete documentation with error handling
- Python re module — Regular expression reference for the multi-strategy parser