Skip to content

Fixing "module 'openai' has no attribute 'ChatCompletion'"

9 min read · updated August 11, 2026

Nothing is wrong with your code in the sense you are looking for. The openai package removed its module-level API in version 1.0.0, and the line that is throwing is written against version 0.x. What you do next depends on which of two different errors you actually have, and they look similar enough to be confused.

What the error means

In openai-python 0.x, the resources were attributes of the module: openai.ChatCompletion, openai.Completion, openai.Embedding. In 1.0.0 those were removed and replaced by an instantiated client, so openai.ChatCompletion.create(...) is calling something that no longer exists.

The maintainers anticipated this, and the package still contains a compatibility shim whose only job is to produce a better error than a bare AttributeError. The shim defines fifteen removed symbols — Edit, File, Audio, Image, Model, Engine, Customer, FineTune, Embedding, Completion, Deployment, Moderation, ErrorObject, FineTuningJob and ChatCompletion — as proxy objects that raise an exception called APIRemovedInV1 when you invoke them. The message names the symbol you touched, tells you the API is no longer supported in openai>=1.0.0, offers openai migrate, and suggests pinning to openai==0.28 as the alternative. The wording lives in the shim source, and the guide it points at is OpenAI’s migration discussion.

Which of the two errors you have

Read the exception type, not just the text. There are two distinct failures and they mean different things.

APIRemovedInV1

You are on 1.x, the shim is doing its job, and the symbol you used is one of the fifteen. This is the ordinary case. The fix is the call-site rewrite below.

A genuine AttributeError

The literal string module 'openai' has no attribute 'ChatCompletion' with an AttributeError type means the shim did not answer, and there are three ordinary reasons.

  • The symbol is not one of the fifteen. The most common by far is openai.error. Old code writes except openai.error.RateLimitError, error is not a shimmed name, and you get a plain AttributeError raised from inside an except clause — which makes the traceback point at error handling rather than at the request.
  • Something is shadowing the package. A file called openai.py in your working directory, or a stray openai/ folder, imports instead of the library and has no attributes at all. This is the version of the error where the code “worked yesterday” and no dependency changed.
  • A partial or mixed install. Two site-packages directories, a virtualenv that is not the interpreter you are running, or a wheel that failed halfway. Here the module imports but is not the module you think.

A detail that sends people down the wrong path: openai.api_key is not one of the fifteen removed symbols. Assigning to it on 1.x does not raise, because a module-level configuration global still exists to support the module-level convenience client. So the line that configures the library appears to work perfectly, and only the line that uses it fails. This is why the error so often reads as “the library is broken” rather than “my code is written against the previous major” — the setup half of the old pattern is silent, and only the call half speaks.

Confirming it in one command

Both diagnoses come from the same two facts — which version, and which file:

python -c "import openai, sys; print(openai.__version__); print(openai.__file__); print(sys.executable)"

A version of 1.x with a path inside site-packages confirms the ordinary migration case. A path pointing at your project directory is the shadowing case: rename your file, delete the neighbouring __pycache__, and the error disappears without any code change. An interpreter path that is not the virtualenv you installed into is the third case, and no amount of editing call sites will fix it.

The minimal diff per call pattern

Each of these is the smallest edit that makes the pattern work on 1.x. Construct the client once at module scope rather than per call — it holds a connection pool, and building one per request throws that away.

# chat completion
- openai.api_key = KEY
- resp = openai.ChatCompletion.create(model=M, messages=msgs)
- text = resp["choices"][0]["message"]["content"]
+ from openai import OpenAI
+ client = OpenAI(api_key=KEY)
+ resp = client.chat.completions.create(model=M, messages=msgs)
+ text = resp.choices[0].message.content

# embeddings
- vec = openai.Embedding.create(model=M, input=s)["data"][0]["embedding"]
+ vec = client.embeddings.create(model=M, input=s).data[0].embedding

# async
- resp = await openai.ChatCompletion.acreate(model=M, messages=msgs)
+ from openai import AsyncOpenAI
+ aclient = AsyncOpenAI(api_key=KEY)
+ resp = await aclient.chat.completions.create(model=M, messages=msgs)

# exceptions
- except openai.error.RateLimitError:
+ except openai.RateLimitError:
- except openai.error.InvalidRequestError:
+ except openai.BadRequestError:

The two that catch people out are in there deliberately. The async form is not a renamed method but a different client class, so acreate has no direct substitute. And InvalidRequestError is the one exception that was renamed rather than moved, to BadRequestError. The full walk-through of the rest of the upgrade is in the v0 to v1 migration page.

If the failing line is not in code you wrote, it is a dependency. An older library that imports openai at module scope and calls openai.ChatCompletion will raise from inside its own file even though your application never touches the old API. Read the frame above the error before editing anything: the fix there is to upgrade that library, not to change yours.

The hasattr trap

The shim raises on invocation, not on attribute access. That is deliberate — the proxy defers the error so that merely looking at the attribute does not explode — but it has a consequence worth knowing before you write a compatibility layer.

# This does NOT do what it looks like it does.
if hasattr(openai, "ChatCompletion"):
    resp = openai.ChatCompletion.create(...)   # 1.x: True, then raises
else:
    resp = client.chat.completions.create(...)

On 1.x the attribute exists, so the branch is taken, and the call inside it raises. Version detection that has to work on both lines must ask a question the shim cannot answer positively — the presence of the new surface rather than the absence of the old:

import openai
NEW_SDK = hasattr(openai, "OpenAI")   # the client class only exists on 1.x

Better still, do not write the branch. A dual-mode wrapper doubles the surface you have to test and tends to outlive the migration by years. If you genuinely must support both for a while, isolate it in one module with one function per operation, and delete that module the week the last caller moves.

When pinning is the right answer

pip install openai==0.28.1 is a legitimate immediate action and a bad medium-term plan. It is right when the failure is in production right now and the migration is an afternoon of work you do not have at this minute; it buys you a green build in one command.

It stops being right quickly. The 0.x line does not receive fixes, and more to the point it does not know about endpoints, parameters or models added since — a pinned 0.28.1 cannot reach anything the API gained afterwards, so the pin quietly becomes a ceiling on what your application can do. Record the pin as an issue with the branch already started, not as a resolution.

Version numbers here are the ones at the time of writing. Before relying on any mapping above, check what your environment actually has with the one-line command in the confirming section — the shim, its list of fifteen symbols, and the exact wording of its message are all things the maintainers can change in a patch release.