Howwise RSS
AI & Automation5 min read

ChatCompletion Object Is Not Subscriptable: Cause and Fix

Short answer

The ChatCompletion object is not subscriptable error appears because openai>=1.0.0 returns Pydantic objects, not dicts. Change response['choices'][0]['message']['content'] to response.choices[0].message.content.

Key takeaways
  • Replace every dictionary-style lookup on a v1 response with attribute access: response.choices[0].message.content.
  • Create a client with OpenAI() and call client.chat.completions.create(...) instead of the removed openai.ChatCompletion.create(...).
  • Use response.model_dump() or model_dump_json() when you need a real dict or JSON string for logging and storage.
  • Run pip show openai with the same interpreter your app uses, because multiple virtualenvs are the most common source of confusion.
  • Pinning openai==0.28.1 works as a stopgap, but it leaves you on an unmaintained SDK without newer models and features.

The one-line fix

If your code indexes the response like a dictionary, switch to attribute access. In openai 1.0.0 and later, the response is a typed object, so square brackets fail with TypeError: 'ChatCompletion' object is not subscriptable.

The same change fixes the sibling error 'ChatCompletionMessage' object is not subscriptable, which appears when you write message['content'] on a message object.

# Old (openai<1.0): raises TypeError on v1
text = response['choices'][0]['message']['content']

# New (openai>=1.0)
text = response.choices[0].message.content

Why does the ChatCompletion object become not subscriptable in v1?

Before 1.0, the library returned an OpenAIObject, a dict subclass, so response['choices'] worked. Version 1.0.0 was rewritten on top of Pydantic models and httpx. Responses are now instances of classes such as ChatCompletion, Choice and ChatCompletionMessage.

Those classes define fields as attributes and do not implement __getitem__. Python raises TypeError whenever you use [] on an object without that method.

Most tutorials were written for the old SDK, which is why copied code fails right after a fresh pip install openai.

Full before and after: the client call pattern

The module-level functions were removed too. Calling openai.ChatCompletion.create on v1 raises an error saying you tried to access openai.ChatCompletion, which is no longer supported in openai>=1.0.0. So the call pattern must change, not only the response access.

In v1 you instantiate a client. It reads OPENAI_API_KEY from the environment if you do not pass api_key.

# BEFORE (openai==0.28.x)
import openai
openai.api_key = "sk-..."
response = openai.ChatCompletion.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Hello"}],
)
print(response['choices'][0]['message']['content'])

# AFTER (openai>=1.0)
from openai import OpenAI

client = OpenAI()  # uses OPENAI_API_KEY, or OpenAI(api_key="...")
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Hello"}],
)
print(response.choices[0].message.content)
Warning: Do not hardcode API keys in source files you commit. Prefer the OPENAI_API_KEY environment variable or a secrets manager.

Old vs new cheat sheet: streaming, embeddings, errors, async

The same dictionary-to-attribute change applies across the SDK. This table covers the places where the next errors usually appear.

In streaming, delta.content is None on some chunks (the first chunk carries the role, the last carries the finish reason), so guard against None before concatenating strings.

TaskOld (0.28)New (>=1.0)
Chat textresponse['choices'][0]['message']['content']response.choices[0].message.content
Create callopenai.ChatCompletion.create(...)client.chat.completions.create(...)
Stream chunkchunk['choices'][0]['delta'].get('content')chunk.choices[0].delta.content (may be None)
Embeddingsopenai.Embedding.create(...)['data'][0]['embedding']client.embeddings.create(...).data[0].embedding
Imagesopenai.Image.create(...)['data'][0]['url']client.images.generate(...).data[0].url
Tool callsmessage['function_call']message.tool_calls[0].function.arguments
Errorsopenai.error.RateLimitErroropenai.RateLimitError
Asyncawait openai.ChatCompletion.acreate(...)await AsyncOpenAI().chat.completions.create(...)
Azureopenai.api_type = 'azure'AzureOpenAI(azure_endpoint=..., api_version=...)
from openai import OpenAI
client = OpenAI()

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Count to 5"}],
    stream=True,
)
parts = []
for chunk in stream:
    if not chunk.choices:
        continue  # some providers send empty-choices chunks
    delta = chunk.choices[0].delta.content
    if delta is not None:
        parts.append(delta)
print("".join(parts))

How do I use async, Azure, errors and tool calls in v1?

For async code, use AsyncOpenAI and await the same method path. The old acreate methods no longer exist.

Azure users must use AzureOpenAI, and the model argument is your deployment name. The api_version and endpoint are passed to the client constructor, not set as module globals.

Exceptions moved to the top level. The base class is openai.OpenAIError, and API failures subclass openai.APIError, including RateLimitError, APIConnectionError and APITimeoutError. The client also retries some failures by default (2 retries), configurable with max_retries.

Tool calls are objects as well. Use message.tool_calls[0].function.name and .arguments, where arguments is a JSON string you parse with json.loads. Calling .get() on any response object fails with AttributeError for the same reason as subscripting.

import asyncio, json, openai
from openai import AsyncOpenAI, AzureOpenAI

aclient = AsyncOpenAI(max_retries=3)

async def main():
    try:
        r = await aclient.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": "Hi"}],
        )
        print(r.choices[0].message.content)
    except openai.RateLimitError:
        print("Rate limited, back off")
    except openai.APIError as e:
        print("API error:", e)

asyncio.run(main())

# Azure
azure = AzureOpenAI(
    api_key="...",
    api_version="2024-02-01",
    azure_endpoint="https://YOUR-RESOURCE.openai.azure.com",
)

# Tool call arguments
# args = json.loads(msg.tool_calls[0].function.arguments)

How do I get a dict, auto-migrate, or pin the old version?

If downstream code needs a dict, for example for logging or a database, convert the model. Use model_dump() for a dict and model_dump_json() for a string. Use json.loads on the string only if you need the dict from the JSON path.

The SDK also ships a migration tool, openai migrate, which rewrites old module-level calls into the client pattern. It may need Grit installed and it edits files in place, so commit or back up first and review the diff afterward.

Pinning is a temporary option: pip install openai==0.28.1. It keeps old code running, but you stay on an SDK that gets no new features, and newer models and APIs will not be supported. Use it only to unblock a deadline.

  1. Run pip show openai, or python -c "import openai; print(openai.__version__)", using the same interpreter that runs your app.
  2. If the version differs from what you expect, check which environment is active (which python, or python -m pip show openai) and activate the right virtualenv.
  3. Commit your work, then run openai migrate in the project directory and review the diff.
  4. Search the project for remaining ['choices'], .get( and openai.error usage and convert them.
  5. Run tests with a real request to confirm the response parses.
data = response.model_dump()          # dict
raw = response.model_dump_json()      # JSON string
print(data["choices"][0]["message"]["content"])
Warning: openai migrate rewrites files in place. Commit or back up first. Also remember that pip install in one environment does not change another, so a fix can appear to do nothing if the wrong interpreter runs.

Common questions

Why do I still get the error after pinning or upgrading?

Your app is probably running in a different environment than the one you changed. Use python -m pip show openai with the exact interpreter or IDE kernel that runs the code, and check for notebooks, Docker images or system Python.

Can I make old response['choices'] code work without editing it?

Not with v1 objects. You could wrap the result with response.model_dump() to get a dict, but you would still need to change the client call because openai.ChatCompletion.create is removed, so a proper migration is cleaner.

Why is delta.content None when streaming?

Streaming chunks carry partial data. The first chunk usually holds only the role and the final chunk holds finish_reason, so content is None on those. Check for None before appending to your output string.

Does response.choices[0].message.content ever return None?

Yes. It is None when the model answers with tool calls instead of text, and when content is filtered or refused in some cases. Check message.tool_calls and finish_reason before using the text.

Related topics:openaipythontypeerrormigrationchat-completions