Image Generation

Ma Mahalakshmi V Updated 16 Sep 2026
13 min read ·Lesson 28 of 72

From Understanding Images to Creating Them

Lessons 1 and 2 covered giving the model images and documents as input — visual content the model reads and reasons about. This lesson covers the reverse direction: generating new images from a text description, a capability exposed through a dedicated image-generation interface rather than through client.responses.create(). Image generation is a genuinely different kind of model and a genuinely different kind of task from everything else in this course — instead of understanding existing content, the model is producing new pixel content from scratch, guided entirely by a text description (a "prompt," using the term in its more general sense, not this course's specific input/instructions terminology).

The Basic Generation Call

result = client.images.generate(
    model="gpt-image-1",
    prompt="A minimalist logo for a coffee roasting company, featuring a simple line-art coffee bean, black and white, clean and modern.",
    size="1024x1024",
)

image_data = result.data[0]
print(image_data.url)         # a URL to the generated image, if the platform returns one
# or, depending on the requested response format:
print(image_data.b64_json)    # base64-encoded image data directly, if requested instead

Note: The exact model name for image generation, the parameter used to choose between a returned URL and inline base64 data, and the specific set of supported size values are all details that can vary by SDK version and by which image-generation model is current at the time you're building. Confirm the current model name, parameter names, and supported sizes against your installed SDK version's documentation before relying on any of these specifics in production code.

Unlike client.responses.create(), which returns a single unified response object regardless of what content it contains, image generation is exposed through its own dedicated method (client.images.generate() here) — a reflection of the fact that image generation is a genuinely separate capability with its own distinct parameters (image size, style, quality level) that don't map onto the Responses API's parameter set at all.

Saving a Generated Image to Disk

Once you have either a URL or base64 data back from a generation call, getting the actual image onto disk (to serve it, to store it, to display it) is a small, standard piece of code worth having ready.

import base64
import requests

def save_generated_image(image_data, output_path: str) -> None:
    if getattr(image_data, "b64_json", None):
        image_bytes = base64.b64decode(image_data.b64_json)
        with open(output_path, "wb") as f:
            f.write(image_bytes)
    elif getattr(image_data, "url", None):
        response = requests.get(image_data.url)
        response.raise_for_status()
        with open(output_path, "wb") as f:
            f.write(response.content)
    else:
        raise ValueError("Generated image data contains neither a URL nor base64 content")

result = client.images.generate(model="gpt-image-1", prompt="A simple sunrise over mountains, watercolor style.", size="1024x1024")
save_generated_image(result.data[0], "sunrise.png")

This function handles both possible response shapes (a URL that needs a separate fetch, or already-inline base64 data) uniformly, which is worth doing since which shape you get can depend on a parameter you set on the generation call, and code that only handles one shape will break unexpectedly if that parameter's default or your own configuration ever changes.

Prompting for Image Generation Is a Different Skill

Everything Unit 3 covered about prompting a text model — clarity, examples, instructions — applies in spirit to image generation prompts too, but the specific techniques that work well are different, since the "reasoning" happening is fundamentally about translating a description into visual composition rather than about logical or factual reasoning over text.

# Vague prompt — leaves too much to chance
vague_prompt = "a nice logo for a company"

# Specific prompt — describes composition, style, color, and mood explicitly
specific_prompt = (
    "A modern, minimalist logo for a tech startup called 'Nimbus'. "
    "Simple geometric cloud shape in a gradient from light blue to white, "
    "flat design, no text, suitable for use as a small app icon, "
    "centered composition on a transparent background."
)

Effective image-generation prompts tend to be specific about composition (what's where), style (photographic, illustrated, flat design, watercolor, and so on), color palette, mood or lighting, and any hard constraints (no text, transparent background, a specific aspect ratio) — the more of these dimensions a prompt addresses explicitly, the less the result depends on the model's own default assumptions, which may not match what you had in mind.

Generating Multiple Variations

Many image-generation interfaces support requesting several images from a single prompt in one call, letting you review a handful of variations rather than making several separate calls.

result = client.images.generate(
    model="gpt-image-1",
    prompt="A cozy reading nook with a window, soft natural light, illustrated style.",
    size="1024x1024",
    n=4,  # request four variations
)

for i, image_data in enumerate(result.data):
    save_generated_image(image_data, f"reading_nook_variant_{i}.png")

Requesting several variations at once is often a more efficient way to explore a prompt's range of plausible outputs than repeatedly regenerating one image at a time, since the model's output for a given prompt has inherent variability — much as text generation does — and seeing several variations side by side makes it easier to judge whether a prompt reliably produces the intended result or only occasionally does.

Editing an Existing Image

Beyond generating an image from scratch, some image-generation interfaces support editing an existing image — modifying a specific region while leaving the rest intact, guided by a mask and a text description of the desired change.

with open("original_photo.png", "rb") as image_file, open("mask.png", "rb") as mask_file:
    result = client.images.edit(
        model="gpt-image-1",
        image=image_file,
        mask=mask_file,
        prompt="Replace the masked area with a clear blue sky.",
    )
save_generated_image(result.data[0], "edited_photo.png")

The mask argument here is itself an image — typically one where transparent (or otherwise designated) regions mark exactly which part of the original image should be replaced, while the rest of the mask indicates content to preserve unchanged. Producing a good mask is its own small design task, usually done with an image-editing tool rather than generated programmatically, though a mask can also be produced by ordinary image-processing code (drawing a shape, thresholding based on color) when the region to edit follows a predictable, programmatically describable pattern.

Note: The exact parameters, mask format, and supported editing operations for image editing are specific to the image-generation model and SDK version in use, and have evolved meaningfully across model generations. Confirm current capabilities and the exact mask format expected against your installed SDK version's documentation before building a production editing feature around this capability.

Cost Considerations for Image Generation

Image generation is typically priced differently from text generation — often per image rather than per token, with cost varying by resolution and by how many variations are requested in a single call. This has a direct, practical implication for how a feature using image generation should be designed and tested.

def estimate_generation_cost(num_images: int, cost_per_image: float) -> float:
    """A simple illustrative calculation — confirm actual current per-image
    pricing against official documentation, since it varies by resolution
    and by which generation model is used."""
    return num_images * cost_per_image

# Testing a prompt-refinement loop by regenerating many times is a genuinely
# more expensive habit than testing a text prompt by regenerating many times,
# since each image generation call typically costs meaningfully more than a
# short text completion
print(f"Estimated cost for 20 test generations: ${estimate_generation_cost(20, 0.04):.2f}")

This cost structure is worth keeping in mind specifically when iterating on a prompt during development: a workflow that regenerates an image dozens of times while refining wording accumulates cost far faster than the equivalent iteration on a short text prompt would, making it worth investing more upfront care in a detailed, well-considered prompt (following the specificity guidance earlier in this lesson) before generating, rather than treating generation as a cheap trial-and-error loop the way a quick text completion often can be.

Handling Content Policy Rejections

Image-generation requests, like text requests, can be declined for content-policy reasons — a prompt describing content the platform doesn't generate, regardless of intent. This is directly analogous to the refusal handling Unit 6, Lesson 4 covered for structured text outputs, and deserves the same explicit handling rather than an assumption that every generation request will succeed.

def generate_image_safely(prompt: str, size: str = "1024x1024") -> dict:
    try:
        result = client.images.generate(model="gpt-image-1", prompt=prompt, size=size)
        return {"success": True, "data": result.data[0]}
    except Exception as e:
        # Depending on the SDK version, a content-policy rejection may be a
        # specific exception type distinguishable from other request failures —
        # confirm current exception types against your SDK version.
        return {"success": False, "error": str(e)}

outcome = generate_image_safely("A peaceful mountain landscape at sunset.")
if outcome["success"]:
    save_generated_image(outcome["data"], "landscape.png")
else:
    print(f"Generation failed: {outcome['error']}")

For a user-facing feature that generates images from user-supplied prompts, surfacing a clear, non-technical message on rejection (rather than a raw exception message) and logging the underlying reason for later review is worth building in from the start, mirroring the same care Unit 6 argued for around refusal handling in text-based structured extraction.

Combining Generated Images With the Responses API

A generated image can be fed back into client.responses.create() as input_image (Lesson 1), letting a single workflow generate an image and then have the model evaluate, describe, or reason about its own output — useful for a review step, an automated quality check, or a multi-step creative workflow.

result = client.images.generate(model="gpt-image-1", prompt="A friendly cartoon robot mascot, simple flat design.", size="1024x1024")
save_generated_image(result.data[0], "mascot.png")

with open("mascot.png", "rb") as f:
    base64_image = base64.b64encode(f.read()).decode("utf-8")

review_response = client.responses.create(
    model="gpt-5.6-luna",
    input=[{"role": "user", "content": [
        {"type": "input_text", "text": "Does this image look like a friendly, approachable mascot suitable for a children's app? Explain briefly."},
        {"type": "input_image", "image_url": f"data:image/png;base64,{base64_image}"},
    ]}],
)
print(review_response.output_text)

This generate-then-review pattern is a practical way to add an automated quality gate to an image-generation pipeline — while keeping in mind Lesson 1's caution about vision reliability: a model's own self-assessment of subjective qualities like "friendliness" is a genuinely softer signal than a factual visual check (verifying a specific required element is present, say), and shouldn't be trusted as a substitute for human review in a workflow where the final output genuinely matters to get right.

Testing Image-Generation Code Without Incurring Cost

Given the per-image cost structure discussed earlier, testing the logic around image generation — prompt construction, saving, error handling — without actually generating real images on every test run is worth doing deliberately, following this course's established dependency-injection pattern.

class FakeImageData:
    def __init__(self, b64_json=None, url=None):
        self.b64_json = b64_json
        self.url = url

def test_save_generated_image_handles_base64():
    import tempfile, os
    fake_data = FakeImageData(b64_json=base64.b64encode(b"fake image bytes").decode("utf-8"))
    with tempfile.TemporaryDirectory() as tmp_dir:
        path = os.path.join(tmp_dir, "test_image.png")
        save_generated_image(fake_data, path)
        with open(path, "rb") as f:
            assert f.read() == b"fake image bytes"
    print("PASS: save_generated_image correctly writes decoded base64 content")

test_save_generated_image_handles_base64()

This test exercises the base64-decoding and file-writing logic in save_generated_image() without ever calling client.images.generate() — the same cost-avoidance motivation behind every fake-object test in this course, applied here to a capability where the cost of a real call is a more direct, per-invocation dollar amount than the token-based cost of a text completion.

Choosing an Appropriate Size and Quality Level

Image-generation interfaces typically expose both a size parameter (the pixel dimensions of the output) and, in some cases, a separate quality or fidelity setting — and, mirroring Unit 3's reasoning-effort discussion for text models, higher settings on either dimension generally cost more and take longer to generate, without being universally "better" for every use case.

def choose_generation_settings(use_case: str) -> dict:
    """A simple mapping from use case to reasonable generation settings —
    confirm exact supported values and cost differences against current docs."""
    settings = {
        "app_icon": {"size": "512x512"},
        "blog_header": {"size": "1536x1024"},
        "print_poster": {"size": "1792x1024"},
    }
    return settings.get(use_case, {"size": "1024x1024"})

icon_settings = choose_generation_settings("app_icon")
result = client.images.generate(model="gpt-image-1", prompt="A simple weather app icon, sun and cloud.", **icon_settings)

A small app icon rarely benefits from the highest available resolution — the extra detail is invisible at the size it's actually displayed, and generating at an unnecessarily high resolution wastes cost without a corresponding visible improvement, exactly analogous to Unit 5's point that not every request benefits equally from every available capability. Matching generation settings to how the image will actually be used, rather than defaulting to the maximum available size for every request, is a straightforward, low-effort cost optimization worth building into any feature that generates several images for different purposes.

Building a Simple Prompt Template System

For an application that generates images in a consistent style across many different subjects — a set of icons for an app, a series of blog post headers — building a reusable prompt template rather than writing each prompt from scratch keeps the visual style consistent and makes it easy to adjust the whole set by changing one shared piece of wording.

ICON_STYLE_TEMPLATE = (
    "A simple, flat-design icon of {subject}, minimalist style, "
    "single accent color on a white background, no text, centered composition."
)

def generate_icon(subject: str, output_path: str) -> None:
    prompt = ICON_STYLE_TEMPLATE.format(subject=subject)
    result = client.images.generate(model="gpt-image-1", prompt=prompt, size="512x512")
    save_generated_image(result.data[0], output_path)

for subject, filename in [("a coffee cup", "icon_coffee.png"), ("a calendar", "icon_calendar.png"), ("a gear", "icon_settings.png")]:
    generate_icon(subject, filename)

This pattern — a shared template string with a single varying piece of content — is directly analogous to Unit 3's discussion of instructions versus input: the template captures the stable, style-defining part of the prompt (the part that should stay consistent across every generated icon), while subject captures the part that varies per call, keeping the whole icon set visually coherent without needing to retype the full style description for every single image.

Common Mistakes

Iterating on a prompt by repeatedly regenerating images, treating image generation like the cheap, fast iteration loop that's reasonable for short text completions, when the per-image cost structure makes many rounds of trial-and-error meaningfully more expensive.

Writing a vague, underspecified prompt and expecting the model to guess the intended composition, style, and mood, when explicit detail across these dimensions (covered earlier in this lesson) substantially improves how closely a generated image matches what was actually wanted.

Not handling content-policy rejections explicitly for a user-facing generation feature, leaving a rejected request to surface as a raw, confusing exception rather than a clear message and a logged reason for review.

Assuming a generation response always returns either a URL or base64 data specifically, and writing code that only handles one shape, when the actual shape returned can depend on a request parameter that might change.

Best Practices

Invest in a detailed, specific prompt before generating, addressing composition, style, color, mood, and any hard constraints explicitly, rather than relying on many cheap regeneration attempts to arrive at an acceptable result — a cost-conscious habit that also tends to produce more consistent, predictable output.

Request multiple variations in a single call when exploring a prompt's range of outputs, rather than making several separate calls, to get a representative sample of what a given prompt reliably produces.

Handle content-policy rejections explicitly, with a clear user-facing message and a logged underlying reason, exactly as Unit 6 recommended for text-based refusals.

Write code that handles both possible response shapes (URL and base64) uniformly, rather than assuming one specific shape will always be returned.

Test the logic around image generation — prompt construction, saving, error handling — with fake image data, reserving real generation calls for final validation, given the meaningfully higher per-call cost of image generation compared to short text completions.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.out Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Mem Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources
Ask about this post
AI Ask about this post

Ask questions about Image Generation and get answers drawn from it.

Signed-in readers only.