Skip to content
, ,

OpenAI API Python Tutorial: Responses API with GPT-6 (2026)

9–13 minutes
Surprised woman in a black hoodie by a city-view window holding a card with the OpenAI logo, hook text: GPT-6 API Python guide

The Responses API is now the main way to call OpenAI models from Python. It handles text, images, files, tools and multi-turn chats through one endpoint.

This tutorial walks you through it step by step with the current GPT-6 models. You will go from an API key to streaming, structured outputs, function calling, caching, batches and error handling.

TLDR

  • Install: pip install openai, set OPENAI_API_KEY, then call client.responses.create().
  • Models: gpt-6-astra (flagship), gpt-6.1-sol (balanced), gpt-6.1-terra (new on October 26, 2026) and gpt-6-luna (cheapest).
  • Structured outputs: pass a Pydantic class to client.responses.parse() and read output_parsed.
  • Built-in tools: web_search and file_search run on OpenAI’s side.
  • Save money: caching is automatic from 1,024 tokens, and the Batch API costs 50% less.
  • Errors: catch RateLimitError and retry with backoff. A 429 can also mean your credits ran out.

Step 1: Get an API key

The API is billed separately from ChatGPT. A ChatGPT Plus or Pro plan does not include API usage by default, so you need credits on the platform.

  1. Sign in to the platform. Go to platform.openai.com and log in or create an account.
  2. Add billing. Open the billing settings and buy prepaid credits. Without credits, calls fail with a 429 quota error.
  3. Create a key. Open API keys, click Create new secret key, and pick the project it belongs to.
  4. Copy it once. The full key is shown only at creation, so store it in a password manager.
  5. Set an environment variable. Never paste the key into your code or a Git repo.
# macOS / Linux
export OPENAI_API_KEY="sk-proj-your-key"

# Windows PowerShell
$env:OPENAI_API_KEY="sk-proj-your-key"

Note: Since October 28, 2026, eligible ChatGPT Pro and Business subscribers can claim up to $500 per month in API credits. Check your platform billing page to see if you qualify.

Step 2: Install the openai SDK

Use a virtual environment so the SDK stays separate from other projects. Then install the official package.

python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install openai pydantic

The client reads OPENAI_API_KEY from your environment automatically. You can create it with no arguments:

from openai import OpenAI

client = OpenAI()

Which model to use

OpenAI’s model page lists four main GPT-6 models. They all have a 1.05M token context window and up to 128K output tokens.

Model ID Best for Price per 1M tokens (input / output) Reasoning levels
gpt-6-astra Hard reasoning and coding; OpenAI’s suggested starting point $10 / $50 low to max
gpt-6.1-sol Balance of quality and cost $2 / $10 low to max
gpt-6.1-terra Cheaper mid-tier work, visual reasoning $1 / $5 none to max
gpt-6-luna High-volume, cost-sensitive tasks $0.10 / $0.50 none to max

GPT-5.x models are still on the API for older code, but new projects should start on GPT-6. For every rate, including cached input and tools, see my OpenAI API pricing guide.

Tip: Build and test with gpt-6-luna first. It is 100 times cheaper than Astra on input, so bugs in your loop cost almost nothing.

Your first text request

A Responses call needs only a model and an input. The output_text property joins all the text the model returned.

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-6-luna",
    instructions="You are a friendly Python tutor. Keep answers short.",
    input="Explain what a Python list comprehension is.",
)

print(response.output_text)
print(response.usage)

Here is what each part does:

  • instructions: the system-level guidance for this call.
  • input: a string, or a list of messages and content parts.
  • usage: input, output and cached token counts, so you can track cost.

To control how hard the model thinks, add a reasoning setting. Lower effort is faster and cheaper:

response = client.responses.create(
    model="gpt-6.1-sol",
    reasoning={"effort": "low"},
    input="Give me three names for a coffee shop.",
)

Multi-turn conversations

The Responses API can keep the chat history for you. Pass the previous response’s ID and only send the new message.

first = client.responses.create(
    model="gpt-6-luna",
    input="My name is Priya and I love hiking.",
)

second = client.responses.create(
    model="gpt-6-luna",
    previous_response_id=first.id,
    input="What hobby did I mention?",
)
print(second.output_text)

You can also manage history yourself by sending a list of messages each time. That gives you full control, which helps when you trim old turns.

Streaming responses

Streaming shows text as it is generated, which makes chat apps feel fast. Set stream=True and loop over the events.

stream = client.responses.create(
    model="gpt-6-luna",
    input="Write a short poem about the sea.",
    stream=True,
)

for event in stream:
    if event.type == "response.output_text.delta":
        print(event.delta, end="", flush=True)
    elif event.type == "response.completed":
        print()
        print(event.response.usage)

Each event has a type. You mostly care about the text deltas and the final completed event, which carries usage data.

Structured outputs with Pydantic

Structured outputs force the model to return JSON that matches your schema. This is the cleanest way to extract data.

from pydantic import BaseModel

class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]

response = client.responses.parse(
    model="gpt-6-luna",
    input=[
        {"role": "system", "content": "Extract the event information."},
        {"role": "user", "content": "Alice and Bob are going to a science fair on Friday."},
    ],
    text_format=CalendarEvent,
)

event = response.output_parsed
print(event.name, event.date, event.participants)

Without Pydantic, pass a raw JSON schema in text={"format": {"type": "json_schema", ...}} with "strict": True. Safety refusals are detectable in code, so check that a parsed result exists before you use it.

Function calling

Function calling lets the model ask your code to run a function. You describe the tool, the model returns a call with arguments, and you send back the result.

import json

tools = [{
    "type": "function",
    "name": "get_weather",
    "description": "Get the current weather for a city.",
    "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
        "additionalProperties": False,
    },
    "strict": True,
}]

def get_weather(city):
    return {"city": city, "temp_c": 21, "sky": "sunny"}  # call a real API here

input_list = [{"role": "user", "content": "What's the weather in Lisbon?"}]
response = client.responses.create(model="gpt-6-luna", tools=tools, input=input_list)

input_list += response.output
for item in response.output:
    if item.type == "function_call" and item.name == "get_weather":
        args = json.loads(item.arguments)
        input_list.append({
            "type": "function_call_output",
            "call_id": item.call_id,
            "output": json.dumps(get_weather(**args)),
        })

final = client.responses.create(model="gpt-6-luna", tools=tools, input=input_list)
print(final.output_text)

The model can return zero, one or several calls in one turn, so always loop over response.output. The call_id links each result to its call.

Vision and file inputs

Send an image

Mix text and image parts in one user message. Use a public URL, or a base64 data URL for a local file.

import base64

with open("receipt.png", "rb") as f:
    b64 = base64.b64encode(f.read()).decode()

response = client.responses.create(
    model="gpt-6-luna",
    input=[{
        "role": "user",
        "content": [
            {"type": "input_text", "text": "What is the total on this receipt?"},
            {"type": "input_image", "image_url": f"data:image/png;base64,{b64}"},
        ],
    }],
)
print(response.output_text)

Send a PDF

Upload the file once, then refer to it by ID. You can also pass a file_url for a public PDF.

uploaded = client.files.create(file=open("report.pdf", "rb"), purpose="user_data")

response = client.responses.create(
    model="gpt-6-luna",
    input=[{
        "role": "user",
        "content": [
            {"type": "input_file", "file_id": uploaded.id},
            {"type": "input_text", "text": "Summarize this report in five bullets."},
        ],
    }],
)

Built-in tools: web search and file search

Built-in tools run on OpenAI’s servers, so you write no tool code. Just add them to tools.

Web search

response = client.responses.create(
    model="gpt-6-luna",
    tools=[{"type": "web_search"}],
    input="What did OpenAI release in the API this week?",
)
print(response.output_text)

File search over your documents

File search needs a vector store. You create it, add files, then point the tool at it.

store = client.vector_stores.create(name="knowledge_base")

with open("handbook.pdf", "rb") as f:
    file = client.files.create(file=f, purpose="assistants")

client.vector_stores.files.create_and_poll(vector_store_id=store.id, file_id=file.id)

response = client.responses.create(
    model="gpt-6-luna",
    input="How many vacation days do new staff get?",
    tools=[{"type": "file_search", "vector_store_ids": [store.id]}],
)
print(response.output_text)

Built-in tool calls are billed on top of tokens. Check the tools section of the pricing page before heavy use.

Prompt caching

Caching is on by default. When the start of your prompt repeats, OpenAI reuses the processed prefix and charges far less for it.

  • Minimum size: on GPT-5.6 and newer, the cacheable prefix needs 1,024 visible input tokens.
  • Cached read price: 0.1x the input rate, or 0.05x on GPT-6.1 Sol and GPT-6.1 Terra.
  • Write price: 1.25x input for the default 30-minute cache, or 2.5x for the 12-hour option added on October 20, 2026.
  • Lifetime setting: use prompt_cache_options={"ttl": "12h"} on GPT-5.6 and newer.
  • Per-user accounting: add an optional prompt_cache_key, such as a customer ID.
response = client.responses.create(
    model="gpt-6.1-sol",
    instructions=LONG_SYSTEM_PROMPT,   # keep the stable part first
    input=user_question,
    prompt_cache_key="customer-42",
)
details = response.usage.input_tokens_details
print("cached:", details.cached_tokens)

Put stable content such as instructions, tools and reference docs first, and the changing question last. My prompt caching guide compares OpenAI with Claude and Gemini.

The Batch API

If you do not need answers right away, the Batch API gives a 50% discount. Jobs finish within a 24-hour window.

  1. Write a JSONL file. Each line has a unique custom_id, "method": "POST", "url": "/v1/responses" and a body.
  2. Upload it. Use client.files.create() with purpose="batch".
  3. Create the batch. Pass the file ID, the endpoint and completion_window="24h".
  4. Poll the status. Call client.batches.retrieve() until it is completed.
  5. Download results. Read client.files.content(batch.output_file_id).
import json

rows = [
    {"custom_id": f"q{i}", "method": "POST", "url": "/v1/responses",
     "body": {"model": "gpt-6-luna", "input": q}}
    for i, q in enumerate(["Define API.", "Define SDK.", "Define JSON."])
]
with open("batch.jsonl", "w") as f:
    for r in rows:
        f.write(json.dumps(r) + "\n")

batch_file = client.files.create(file=open("batch.jsonl", "rb"), purpose="batch")
batch = client.batches.create(
    input_file_id=batch_file.id,
    endpoint="/v1/responses",
    completion_window="24h",
)
print(batch.id, batch.status)

A batch can hold up to 50,000 requests and a 200 MB file, and each file can target only one model. Unfinished work after 24 hours is marked expired, and you pay only for completed requests.

Errors and retries

The SDK raises a typed exception for each kind of failure. Catch the ones you can recover from and fail loudly on the rest.

import time
import openai

def ask(prompt, attempts=5):
    for i in range(attempts):
        try:
            return client.responses.create(model="gpt-6-luna", input=prompt)
        except openai.RateLimitError as e:
            if "quota" in str(e).lower() or "credit" in str(e).lower():
                raise  # retrying will not add credits
            time.sleep(2 ** i)
        except (openai.APIConnectionError, openai.APITimeoutError, openai.InternalServerError):
            time.sleep(2 ** i)
    raise RuntimeError("Gave up after retries")

The SDK also retries some errors on its own. You can raise that with OpenAI(max_retries=5) or set a timeout with OpenAI(timeout=60).

Status Python exception Usual cause Fix
400 BadRequestError Bad parameter or schema Read the message and fix the request
401 AuthenticationError Wrong, revoked or mistyped key Check the key and project, or make a new one
403 PermissionDeniedError Unsupported region or no access Check supported countries and key permissions
404 NotFoundError Wrong model name or file ID Check the ID spelling
429 RateLimitError Too fast, or no credits or spend limit hit Back off, or add credits and raise limits
500 / 503 InternalServerError Server issue or overload Retry with longer delays, check the status page

Warning: A 429 with credit_balance_exhausted or a spend limit code will never fix itself with retries. Your loop will just burn time until you add credits or raise the limit.

Troubleshooting common problems

“The api_key client option must be set”

The SDK cannot find OPENAI_API_KEY. Set it in the same terminal that runs Python, or load it from a .env file with python-dotenv.

“Model not found” with a GPT-6 name

Check the exact ID, such as gpt-6.1-sol with the dot. Also check that your project has access to that model, and update the SDK with pip install -U openai.

Rate limits hit far too early

  • Check your tier: on October 6, 2026, OpenAI cut usage tiers from five to three: Build, Launch and Grow. Your limits depend on your tier.
  • Ramp slowly: OpenAI suggests raising traffic gradually, not in one jump.
  • Shared usage: other apps in the same organization share the limit.

cached_tokens is always 0

Your prompt may be under 1,024 tokens, or the start of it changes each call. Move dates, user names and other variable text to the end.

Structured output fails to parse

Every field must be listed in required, and additionalProperties must be false in strict mode. With Pydantic, avoid unsupported types and keep models simple. For other API comparisons, see my AI API pricing comparison.

Frequently Asked Questions

Should I use the Responses API or Chat Completions?

Use the Responses API for new projects. It supports built-in tools, stored conversations and the latest features, and GPT-6 Astra needs it for tool calling.

What is the cheapest OpenAI model for Python projects?

GPT-6 Luna (gpt-6-luna) costs $0.10 per million input tokens and $0.50 per million output tokens. It is the best choice for high-volume, simple tasks.

Does ChatGPT Plus include API access?

No. The API is billed separately through platform credits. Eligible Pro and Business subscribers can claim up to $500 per month in API credits.

How do I stream responses in Python?

Pass stream=True to client.responses.create() and print each response.output_text.delta event’s delta.

How do I get JSON back reliably?

Use structured outputs with client.responses.parse() and a Pydantic model. The result is in response.output_parsed.

How much does the Batch API save?

Batch requests cost 50% less than normal calls. Results arrive within 24 hours, with up to 50,000 requests per batch.

Is prompt caching automatic?

Yes. Prompts of 1,024 tokens or more are cached automatically on GPT-5.6 and newer, and you can see hits in usage.input_tokens_details.cached_tokens.

Sources

Free weekly email

Get new AI guides every week

One short weekly email with our newest local AI, ComfyUI and AI tool guides. No spam, no pop-ups, just the good stuff.

Free. Unsubscribe anytime in one click. We only use your email to send this newsletter. See our privacy policy.

Surprised woman in a black hoodie by a city-view window holding a card with the OpenAI logo, hook text: GPT-6 API Python guide