Image Generation
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
sizevalues 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.