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:

ModelResolutionsQualityCostRecommended use
dall-e-31024x1024, 1792x1024, 1024x1792Superior$0.04-0.12Production, maximum quality
dall-e-2256x256, 512x512, 1024x1024Lower$0.016-0.02Fast 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

AspectDALL-E 2DALL-E 3
Prompt understandingLiteral, simpleAdvanced semantics
Text in imagesVery poorAcceptable (not perfect)
Prompt rewritingNoYes (revised prompt)
Images per requestUp to 10Only 1
Editing (inpainting)YesNo (requires DALL-E 2)
VariationsYesNo

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:

ValueAspectPixelsTypical use
"1024x1024"Square (1:1)1,048,576Avatars, icons, square social media
"1792x1024"Landscape (7:4)1,835,008Banners, headers, desktop wallpapers
"1024x1792"Portrait (4:7)1,835,008Stories, 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

ValueDetailTimeCost (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

ValueEffectWhen to use it
"vivid"More saturated colors, more dramatic composition, more "artistic" styleIllustrations, 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

ValueReturnsUse
"url"Temporary URL (expires in ~1 hour)Quick preview, download later
"b64_json"base64-encoded imageSave 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

SituationImpactHow to handle it
Vague promptsDALL-E adds details you didn't ask forBe specific in your original prompt
Very specific styleDALL-E may alter your styleInclude "I NEED exactly..." at the beginning
Consistency across imagesEach generation rewrites differentlySave the revised_prompt to reproduce
Auditing / loggingYou need to know what was actually generatedAlways 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)

ModelSizeQualityCost per image
dall-e-31024x1024standard$0.040
dall-e-31024x1024hd$0.080
dall-e-31792x1024standard$0.080
dall-e-31792x1024hd$0.120
dall-e-31024x1792standard$0.080
dall-e-31024x1792hd$0.120
dall-e-2256x256$0.016
dall-e-2512x512$0.018
dall-e-21024x1024$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

ProblemProbable causeSolution
content_policy_violationPrompt contains sensitive or ambiguous contentRephrase the prompt; avoid sensitive topics, violence, real people
The revised prompt completely changed my intentDALL-E 3 rewrote too muchUse the "I NEED to test..." prefix or be more explicit
The image has artifacts or low qualityquality="standard" insufficientSwitch to quality="hd"
Rate limit reachedToo many requests per minuteImplement retry with exponential backoff
Image URL doesn't workURLs expire in ~1 hourDownload immediately after generating
Text in the image is illegibleDALL-E 3 isn't perfect with typographyReduce the amount of text; use English prompts for text
The image doesn't match the promptPrompt too vague or conflictingBe more specific; review the revised_prompt
n greater than 1 gives an errorDALL-E 3 only accepts n=1Make multiple requests; use dall-e-2 if you need n>1
Deformed hands or fingersKnown limitation of diffusion modelsAdd "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

  1. OpenAI Images API Reference — Complete API reference
  2. DALL-E 3 Guide — Official guide with best practices
  3. OpenAI Pricing — Up-to-date prices
  4. OpenAI Usage Limits — Rate limits per tier