Module 4: Image Generation
2. DALL-E 3 (OpenAI)
Description
DALL-E 3 is OpenAI's image generation model. It represents the standard in generation quality for closed APIs: advanced understanding of natural-language prompts, high visual consistency, and direct integration with the OpenAI ecosystem. In this capsule you'll master the complete API: all the parameters, output formats, image downloads, handling the "revised prompt", costs, and error patterns.
Key difference from other models: DALL-E 3 rewrites your prompt internally before generating the image. This means the model "improves" your description to get better results, but it also means you don't have absolute control over what it generates. Understanding this behavior is critical to using it in production.
Available Models
OpenAI offers two image generation models:
| Model | Resolutions | Quality | Cost | Recommended use |
|---|---|---|---|---|
| dall-e-3 | 1024x1024, 1792x1024, 1024x1792 | Superior | $0.04-0.12 | Production, maximum quality |
| dall-e-2 | 256x256, 512x512, 1024x1024 | Lower | $0.016-0.02 | Fast prototypes, high volume |
Recommendation: Use dall-e-3 whenever quality matters. dall-e-2 has its place for low-cost prototypes where visual quality is not a priority.
Fundamental differences between DALL-E 2 and DALL-E 3
| Aspect | DALL-E 2 | DALL-E 3 |
|---|---|---|
| Prompt understanding | Literal, simple | Advanced semantics |
| Text in images | Very poor | Acceptable (not perfect) |
| Prompt rewriting | No | Yes (revised prompt) |
| Images per request | Up to 10 | Only 1 |
| Editing (inpainting) | Yes | No (requires DALL-E 2) |
| Variations | Yes | No |
API Parameters
Complete reference
from openai import OpenAI
client = OpenAI()
response = client.images.generate(
model="dall-e-3",
prompt="Image description",
size="1024x1024",
quality="standard",
style="vivid",
response_format="url",
n=1
)
Parameter: size
Controls the output resolution. DALL-E 3 supports exactly three options:
| Value | Aspect | Pixels | Typical use |
|---|---|---|---|
"1024x1024" | Square (1:1) | 1,048,576 | Avatars, icons, square social media |
"1792x1024" | Landscape (7:4) | 1,835,008 | Banners, headers, desktop wallpapers |
"1024x1792" | Portrait (4:7) | 1,835,008 | Stories, posters, vertical portraits |
def generate_with_size(prompt: str, size: str) -> str:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=size,
n=1
)
return response.data[0].url
url_square = generate_with_size("Astronaut cat on the moon", "1024x1024")
url_landscape = generate_with_size("Astronaut cat on the moon", "1792x1024")
url_portrait = generate_with_size("Astronaut cat on the moon", "1024x1792")
Important note: The landscape and portrait resolutions cost more than the square one. Choose based on the real use case, not "just in case".
Parameter: quality
| Value | Detail | Time | Cost (1024x1024) | Cost (1792x1024 or 1024x1792) |
|---|---|---|---|---|
"standard" | Good for most cases | ~10-15s | $0.040 | $0.080 |
"hd" | More detail, finer textures | ~15-25s | $0.080 | $0.120 |
url_standard = client.images.generate(
model="dall-e-3",
prompt="Mountainous landscape with a crystal-clear lake at dawn",
size="1024x1024",
quality="standard",
n=1
).data[0].url
url_hd = client.images.generate(
model="dall-e-3",
prompt="Mountainous landscape with a crystal-clear lake at dawn",
size="1024x1024",
quality="hd",
n=1
).data[0].url
When to use HD: Images where fine detail matters — landscapes with textures, portraits, digital art that will be enlarged. For thumbnails or previews, standard is enough.
Parameter: style
| Value | Effect | When to use it |
|---|---|---|
"vivid" | More saturated colors, more dramatic composition, more "artistic" style | Illustrations, concept art, visual marketing |
"natural" | More faithful colors, more photographic style, less "processed" | Product photography, realistic mockups, documentation |
prompt = "Coffee cup on a wooden desk, morning light"
url_vivid = client.images.generate(
model="dall-e-3",
prompt=prompt,
style="vivid",
n=1
).data[0].url
url_natural = client.images.generate(
model="dall-e-3",
prompt=prompt,
style="natural",
n=1
).data[0].url
Parameter: n
DALL-E 3 only accepts n=1. If you need multiple images from the same prompt, you must make multiple requests:
def generate_multiple(prompt: str, count: int = 3) -> list[str]:
urls = []
for _ in range(count):
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1024x1024",
n=1
)
urls.append(response.data[0].url)
return urls
urls = generate_multiple("Minimalist logo for a tech startup", count=5)
for i, url in enumerate(urls):
print(f"Variation {i+1}: {url}")
Parameter: response_format
| Value | Returns | Use |
|---|---|---|
"url" | Temporary URL (expires in ~1 hour) | Quick preview, download later |
"b64_json" | base64-encoded image | Save directly, process in memory |
Complete Base Function
This is the function you'll reuse throughout the module:
from openai import OpenAI
import base64
import requests
from pathlib import Path
client = OpenAI()
def generate_image_dalle3(
prompt: str,
size: str = "1024x1024",
quality: str = "standard",
style: str = "vivid",
save_path: str | None = None
) -> dict:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=size,
quality=quality,
style=style,
n=1
)
result = {
"url": response.data[0].url,
"revised_prompt": response.data[0].revised_prompt,
"original_prompt": prompt,
"size": size,
"quality": quality,
"style": style,
}
if save_path:
img_data = requests.get(response.data[0].url).content
Path(save_path).write_bytes(img_data)
result["saved_to"] = save_path
return result
Example usage:
result = generate_image_dalle3(
prompt="A friendly robot teaching programming to kids, digital illustration style",
size="1792x1024",
quality="hd",
style="vivid",
save_path="generated/robot_teacher.png"
)
print(f"URL: {result['url'][:80]}...")
print(f"Original prompt: {result['original_prompt']}")
print(f"Revised prompt: {result['revised_prompt']}")
print(f"Saved to: {result.get('saved_to', 'Not saved')}")
Revised Prompts: DALL-E 3's Unique Behavior
DALL-E 3 rewrites your prompt before generating the image. It does this to improve the quality of the result, but it has important implications:
How it works
response = client.images.generate(
model="dall-e-3",
prompt="cat",
size="1024x1024",
n=1
)
print(f"Your prompt: cat")
print(f"Prompt used: {response.data[0].revised_prompt}")
The output will be something like:
Your prompt: cat
Prompt used: A fluffy domestic cat with bright green eyes sitting gracefully on a
windowsill, with soft natural light streaming through the window, captured in a
warm, cozy setting with subtle bokeh in the background.
DALL-E 3 transformed "cat" into a detailed description with position, lighting, style and atmosphere.
Practical implications
| Situation | Impact | How to handle it |
|---|---|---|
| Vague prompts | DALL-E adds details you didn't ask for | Be specific in your original prompt |
| Very specific style | DALL-E may alter your style | Include "I NEED exactly..." at the beginning |
| Consistency across images | Each generation rewrites differently | Save the revised_prompt to reproduce |
| Auditing / logging | You need to know what was actually generated | Always log revised_prompt |
Forcing fidelity to the prompt
If you need DALL-E to respect your prompt as literally as possible:
prompt = """I NEED to test how the tool works with extremely simple prompts.
DO NOT add any detail, just use it AS-IS: A red square on a white background."""
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1024x1024",
n=1
)
print(f"Revised: {response.data[0].revised_prompt}")
This reduces (but doesn't eliminate) the rewriting. DALL-E 3 always modifies the prompt to some degree.
Downloading and Storing Images
Download from URL
The URLs DALL-E 3 returns are temporary (they expire in approximately 1 hour). If you need the image later, download it immediately:
import requests
from pathlib import Path
def download_image(url: str, save_path: str) -> str:
response = requests.get(url, timeout=30)
response.raise_for_status()
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
Path(save_path).write_bytes(response.content)
return save_path
Generate directly in base64
If you don't want to depend on temporary URLs:
def generate_image_b64(prompt: str, save_path: str) -> str:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size="1024x1024",
response_format="b64_json",
n=1
)
img_bytes = base64.b64decode(response.data[0].b64_json)
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
Path(save_path).write_bytes(img_bytes)
return save_path
Batch generation with storage
def generate_batch(prompts: list[str], output_dir: str = "generated") -> list[dict]:
Path(output_dir).mkdir(parents=True, exist_ok=True)
results = []
for i, prompt in enumerate(prompts):
try:
filename = f"{output_dir}/image_{i:03d}.png"
result = generate_image_dalle3(prompt, save_path=filename)
results.append({"status": "success", **result})
print(f"[{i+1}/{len(prompts)}] Generated: {filename}")
except Exception as e:
results.append({"status": "error", "prompt": prompt, "error": str(e)})
print(f"[{i+1}/{len(prompts)}] Error: {e}")
return results
prompts = [
"Futuristic user interface with holographic data",
"Cyberpunk cityscape at sunset with neon",
"Technical diagram of a microservices architecture",
]
results = generate_batch(prompts)
Cost Table
Prices per configuration (USD, 2024-2025)
| Model | Size | Quality | Cost per image |
|---|---|---|---|
| dall-e-3 | 1024x1024 | standard | $0.040 |
| dall-e-3 | 1024x1024 | hd | $0.080 |
| dall-e-3 | 1792x1024 | standard | $0.080 |
| dall-e-3 | 1792x1024 | hd | $0.120 |
| dall-e-3 | 1024x1792 | standard | $0.080 |
| dall-e-3 | 1024x1792 | hd | $0.120 |
| dall-e-2 | 256x256 | — | $0.016 |
| dall-e-2 | 512x512 | — | $0.018 |
| dall-e-2 | 1024x1024 | — | $0.020 |
Monthly cost estimation
def estimate_monthly_cost(
images_per_day: int,
size: str = "1024x1024",
quality: str = "standard"
) -> dict:
prices = {
("1024x1024", "standard"): 0.040,
("1024x1024", "hd"): 0.080,
("1792x1024", "standard"): 0.080,
("1792x1024", "hd"): 0.120,
("1024x1792", "standard"): 0.080,
("1024x1792", "hd"): 0.120,
}
price_per_image = prices.get((size, quality), 0.040)
daily = images_per_day * price_per_image
monthly = daily * 30
return {
"per_image": price_per_image,
"daily": round(daily, 2),
"monthly": round(monthly, 2),
"config": f"{size} / {quality}",
}
print(estimate_monthly_cost(50, "1024x1024", "standard"))
print(estimate_monthly_cost(50, "1792x1024", "hd"))
Output:
{'per_image': 0.04, 'daily': 2.0, 'monthly': 60.0, 'config': '1024x1024 / standard'}
{'per_image': 0.12, 'daily': 6.0, 'monthly': 180.0, 'config': '1792x1024 / hd'}
Error Handling
Content Policy Violations
DALL-E 3 rejects prompts that violate its content policies. The error is a BadRequestError with information about the violation:
from openai import BadRequestError
def generate_safe(prompt: str, **kwargs) -> dict | None:
try:
return generate_image_dalle3(prompt, **kwargs)
except BadRequestError as e:
error_msg = str(e).lower()
if "content_policy" in error_msg or "safety" in error_msg:
return {
"status": "rejected",
"reason": "content_policy",
"prompt": prompt,
"message": "The prompt was rejected by content policy. Modify the description.",
}
raise
Rate Limits
import time
from openai import RateLimitError
def generate_with_retry(prompt: str, max_retries: int = 3, **kwargs) -> dict:
for attempt in range(max_retries):
try:
return generate_image_dalle3(prompt, **kwargs)
except RateLimitError:
wait = 2 ** attempt
print(f"Rate limit reached. Waiting {wait}s...")
time.sleep(wait)
raise Exception(f"Persistent rate limit after {max_retries} attempts")
Timeout
from openai import OpenAI
import httpx
client_with_timeout = OpenAI(
timeout=httpx.Timeout(60.0, connect=10.0)
)
Complete error handling
from openai import BadRequestError, RateLimitError, APITimeoutError, APIError
def generate_robust(prompt: str, **kwargs) -> dict:
try:
return generate_image_dalle3(prompt, **kwargs)
except BadRequestError as e:
return {"status": "error", "type": "content_policy", "detail": str(e)}
except RateLimitError:
return {"status": "error", "type": "rate_limit", "detail": "Too many requests"}
except APITimeoutError:
return {"status": "error", "type": "timeout", "detail": "Generation took too long"}
except APIError as e:
return {"status": "error", "type": "api_error", "detail": str(e)}
Troubleshooting
| Problem | Probable cause | Solution |
|---|---|---|
content_policy_violation | Prompt contains sensitive or ambiguous content | Rephrase the prompt; avoid sensitive topics, violence, real people |
| The revised prompt completely changed my intent | DALL-E 3 rewrote too much | Use the "I NEED to test..." prefix or be more explicit |
| The image has artifacts or low quality | quality="standard" insufficient | Switch to quality="hd" |
| Rate limit reached | Too many requests per minute | Implement retry with exponential backoff |
| Image URL doesn't work | URLs expire in ~1 hour | Download immediately after generating |
| Text in the image is illegible | DALL-E 3 isn't perfect with typography | Reduce the amount of text; use English prompts for text |
| The image doesn't match the prompt | Prompt too vague or conflicting | Be more specific; review the revised_prompt |
n greater than 1 gives an error | DALL-E 3 only accepts n=1 | Make multiple requests; use dall-e-2 if you need n>1 |
| Deformed hands or fingers | Known limitation of diffusion models | Add "anatomically correct hands" to the prompt; not always resolved |
Exercises
Exercise 1: Compare vivid vs natural styles
Generate the same image with style="vivid" and style="natural". Save both and compare visually.
See solution
prompt = "Cozy Italian restaurant at night, lit candles, pasta dishes on the table"
results = {}
for style in ["vivid", "natural"]:
result = generate_image_dalle3(
prompt=prompt,
style=style,
quality="hd",
save_path=f"generated/style_{style}.png"
)
results[style] = result
print(f"\nStyle: {style}")
print(f"Revised prompt: {result['revised_prompt'][:100]}...")
print(f"Saved to: {result['saved_to']}")
print(f"\nCompare the files in generated/ visually")
What to observe: vivid will have warmer colors and more dramatic lighting. natural will be more photographic and subtle.
Exercise 2: Generate the three resolutions and compare
Generate the same image in the three available resolutions. Implement a function that automates the comparison.
See solution
def compare_sizes(prompt: str) -> list[dict]:
sizes = ["1024x1024", "1792x1024", "1024x1792"]
results = []
for size in sizes:
label = {
"1024x1024": "square",
"1792x1024": "landscape",
"1024x1792": "portrait"
}[size]
result = generate_image_dalle3(
prompt=prompt,
size=size,
save_path=f"generated/size_{label}.png"
)
results.append({"size": size, "label": label, **result})
print(f"{label} ({size}): saved")
return results
results = compare_sizes("Futuristic city with glass skyscrapers and vertical gardens")
What to observe: The composition changes drastically with the aspect ratio. Landscape favors panoramas, portrait favors vertical subjects (towers, people).
Exercise 3: Logging system for generation
Create a system that logs each generation with timestamp, original prompt, revised prompt, parameters and estimated cost in a JSON file.
See solution
import json
from datetime import datetime
from pathlib import Path
COST_TABLE = {
("1024x1024", "standard"): 0.040,
("1024x1024", "hd"): 0.080,
("1792x1024", "standard"): 0.080,
("1792x1024", "hd"): 0.120,
("1024x1792", "standard"): 0.080,
("1024x1792", "hd"): 0.120,
}
def generate_with_logging(
prompt: str,
log_file: str = "generated/generation_log.json",
**kwargs
) -> dict:
result = generate_image_dalle3(prompt, **kwargs)
size = kwargs.get("size", "1024x1024")
quality = kwargs.get("quality", "standard")
cost = COST_TABLE.get((size, quality), 0.040)
log_entry = {
"timestamp": datetime.now().isoformat(),
"original_prompt": prompt,
"revised_prompt": result["revised_prompt"],
"size": size,
"quality": quality,
"style": kwargs.get("style", "vivid"),
"cost_usd": cost,
"url": result["url"][:80] + "...",
}
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
logs = []
if log_path.exists():
logs = json.loads(log_path.read_text())
logs.append(log_entry)
log_path.write_text(json.dumps(logs, indent=2, ensure_ascii=False))
total_cost = sum(entry["cost_usd"] for entry in logs)
print(f"Generated. Cost: ${cost}. Cumulative total: ${total_cost:.2f} ({len(logs)} images)")
return result
generate_with_logging("Minimalist flowchart for a CI/CD process", quality="hd")
generate_with_logging("App icon for a fitness app", size="1024x1024")
generate_with_logging("Advertising banner for an online store", size="1792x1024", quality="hd")
Exercise 4: Robust content policy handling with reprompting
Implement a function that, when DALL-E rejects a prompt due to content policy, automatically tries to rephrase it using GPT-4o-mini and retries the generation.
See solution
from openai import BadRequestError
def rephrase_prompt(original_prompt: str) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"The user wants to generate an image with DALL-E 3 but their prompt was rejected "
"by content policy. Rephrase the prompt so it's acceptable while keeping the "
"original intent as much as possible. Respond ONLY with the new prompt."
),
},
{"role": "user", "content": f"Rejected prompt: {original_prompt}"},
],
max_tokens=300,
)
return response.choices[0].message.content.strip()
def generate_with_reprompt(prompt: str, max_attempts: int = 3, **kwargs) -> dict:
current_prompt = prompt
for attempt in range(max_attempts):
try:
result = generate_image_dalle3(current_prompt, **kwargs)
result["attempt"] = attempt + 1
result["was_rephrased"] = attempt > 0
return result
except BadRequestError as e:
if "content_policy" not in str(e).lower() or attempt == max_attempts - 1:
raise
print(f"Attempt {attempt + 1} rejected. Rephrasing prompt...")
current_prompt = rephrase_prompt(current_prompt)
print(f"New prompt: {current_prompt[:100]}...")
raise Exception("Could not generate the image after rephrasing")
result = generate_with_reprompt(
"A dramatic scene of conflict in a city",
save_path="generated/reprompted.png"
)
print(f"Generated on attempt {result['attempt']}, rephrased: {result['was_rephrased']}")
Advanced Patterns
Generation with complete metadata
from PIL import Image
from io import BytesIO
def generate_with_metadata(prompt: str, **kwargs) -> dict:
response = client.images.generate(
model="dall-e-3",
prompt=prompt,
size=kwargs.get("size", "1024x1024"),
quality=kwargs.get("quality", "standard"),
style=kwargs.get("style", "vivid"),
response_format="b64_json",
n=1
)
img_bytes = base64.b64decode(response.data[0].b64_json)
img = Image.open(BytesIO(img_bytes))
return {
"image": img,
"bytes": img_bytes,
"revised_prompt": response.data[0].revised_prompt,
"original_prompt": prompt,
"dimensions": img.size,
"format": img.format,
"mode": img.mode,
"size_bytes": len(img_bytes),
}
meta = generate_with_metadata("Technical diagram of a neural network")
print(f"Dimensions: {meta['dimensions']}")
print(f"Format: {meta['format']}, Mode: {meta['mode']}")
print(f"Size: {meta['size_bytes'] / 1024:.1f} KB")
print(f"Revised prompt: {meta['revised_prompt'][:100]}...")
Concurrent generation
When you need multiple images and latency matters:
from concurrent.futures import ThreadPoolExecutor, as_completed
def generate_concurrent(prompts: list[str], max_workers: int = 3) -> list[dict]:
results = [None] * len(prompts)
def gen(idx: int, prompt: str) -> tuple[int, dict]:
result = generate_image_dalle3(prompt, save_path=f"generated/concurrent_{idx:03d}.png")
return idx, result
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(gen, i, p): i for i, p in enumerate(prompts)}
for future in as_completed(futures):
idx, result = future.result()
results[idx] = result
print(f"[{idx+1}/{len(prompts)}] Completed")
return results
prompts = [
"Minimalist icon of a cloud with a padlock",
"Minimalist icon of a server with arrows",
"Minimalist icon of a database with a gear",
"Minimalist icon of a shield with a checkmark",
]
results = generate_concurrent(prompts)
Note: Respect OpenAI's rate limits. With max_workers=3 you'll rarely have problems, but with more you could get a RateLimitError.
Additional Resources
- OpenAI Images API Reference — Complete API reference
- DALL-E 3 Guide — Official guide with best practices
- OpenAI Pricing — Up-to-date prices
- OpenAI Usage Limits — Rate limits per tier