Fundamentals

Fix SSL: CERTIFICATE_VERIFY_FAILED in Python

Diagnose and repair Python's CERTIFICATE_VERIFY_FAILED error on macOS, behind a corporate proxy, or with a stale certifi bundle, without ever disabling verification.

By the end of this guide you will know which of four causes is breaking your encrypted connection, and you will have fixed it properly in about fifteen minutes. You will also have a one-line test that tells you, before you touch any code, whether the problem is your machine or the API you are calling. Everything here works the same whether you are calling an AI model, downloading a file, or installing a package.

This is one guide in Debugging Python AI Errors, the section that walks through the errors beginners actually hit when their first AI script meets the real internet.

What the error is actually telling you

The full message usually looks like this, buried at the bottom of a long traceback:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify
failed: unable to get local issuer certificate (_ssl.c:1006)

Read it as a sentence about your computer, not about the server. When Python opens an https:// address it performs a TLS handshake — a short exchange where the server proves who it is by presenting a certificate, and your machine decides whether to believe it. That certificate is signed by an intermediate authority, which is signed by a root certificate authority (a small number of organisations whose certificates are shipped with operating systems and browsers). Python walks that chain upward and looks for the root in its own trust store: a plain text file full of trusted root certificates. If the chain does not end somewhere in that file, Python refuses the connection. It never sends your request, so your API key is never used and nothing is billed.

The important consequence is that the fix is always local. You are repairing the file Python reads, or telling Python to read a different one.

Where a TLS handshake breaks and produces a certificate verify error A six-stage flow showing a Python script opening an HTTPS call, the server returning its certificate chain, Python checking the issuer against the certifi trust store, finding no match, and raising an SSL verification error before any request data is sent. 1. Your script opens an HTTPS call 2. Server sends its certificate chain 3. Python checks the chain's issuer 4. Trust store certifi CA bundle 5. No issuer match handshake aborts 6. Python raises an SSL verify error
The failure happens at stage four, inside your own machine, before a single byte of your request or your API key leaves the computer.

Prerequisites

You need Python 3.10 or newer and a project folder you can run scripts from. If Python is not installed yet, pick the guide for your platform: How to Install Python for AI Projects on Mac, How to Install Python for AI on Windows, or How to Install Python for AI on Linux.

Work inside a virtual environment (an isolated folder holding this project's packages) so the certificate package you upgrade below affects only this project. Create a Python Virtual Environment for AI covers it in detail:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "httpx>=0.27" "openai>=1.40" "python-dotenv>=1.0" certifi

Put your key in a .env file in the project folder:

OPENAI_API_KEY=sk-your-key-here

Then add that file to .gitignore before you write another line:

echo ".env" >> .gitignore

This matters more than usual on this page, because the wrong "fix" for a certificate error is exactly what turns a private key into a public one.

Step 1 — Prove it is TLS, not the API

Before changing anything, separate a broken trust store from a broken request. Run this script, which fetches a well-known public page and prints the file Python is using as its trust store:

import ssl
import certifi
import httpx

print("OpenSSL build:", ssl.OPENSSL_VERSION)
print("Trust store file:", certifi.where())

try:
    response = httpx.get("https://www.python.org", timeout=10.0)
    print("Handshake OK, HTTP status:", response.status_code)
except httpx.ConnectError as exc:
    print("Handshake failed:", exc)

A printed status of 200 means TLS is healthy and your problem lies in the API call itself. A Handshake failed line means the trust store is the problem, and every HTTPS address will fail the same way.

Now narrow it further by hitting the API host with no key at all:

import httpx

try:
    response = httpx.get("https://api.openai.com/v1/models", timeout=10.0)
    print("Reached the API. HTTP status:", response.status_code)
except httpx.ConnectError as exc:
    print("Could not complete the handshake:", exc)

An HTTP status of 401 is good news here. It means the handshake succeeded and the server answered — it simply refused an unauthenticated request. Your certificates are fine and you have an authentication problem instead, covered in Fix the 401 Unauthorized Error in OpenAI Python. If instead you see a certificate message, keep going.

The two results split the remaining work cleanly. If every host fails, your local trust store is stale or empty. If only the API host fails while python.org succeeds, something on your network is sitting between you and that host and re-signing the traffic.

Decision tree for choosing the right certificate fix A branching chart: if every HTTPS host fails the trust store is at fault, so run the macOS certificate installer and upgrade certifi; if only one host fails, a proxy is inspecting traffic, so point SSL_CERT_FILE at the company root certificate. Does every HTTPS host fail, or one? Every host fails your trust store Only one host TLS inspection Run Install Certificates.command Upgrade certifi pip install -U certifi Set SSL_CERT_FILE at the company CA
One test splits the whole problem: a universal failure points at your own trust store, a single-host failure points at something on the network re-signing your traffic.

Step 2 — Run the macOS certificate installer

This is the single most common cause, and it catches almost everyone who downloads Python from python.org onto a Mac. That build does not read the certificates in macOS Keychain. It expects to use its own bundled list, and the installer leaves that list unlinked until you run a small script it ships with. Until you do, every HTTPS call from that interpreter fails.

Find your Python folder in Applications and run the file:

ls /Applications | grep "Python"
/Applications/Python\ 3.12/Install\ Certificates.command

Substitute your own version number for 3.12. The script prints a few lines, installs certifi, and links Python's default certificate file to it. Re-run the Step 1 test afterwards; it should print 200.

If there is no Python 3.x folder in Applications, you did not use the python.org installer — you are on Homebrew, pyenv, or the system Python, and this step does not apply to you. Skip to Step 3. You can confirm which interpreter is active with:

which python
python -c "import sys; print(sys.prefix)"

Homebrew and pyenv builds link against your operating system's certificates or against certifi directly, so they rarely need this step. A tidy way to avoid the whole issue on a Mac is to install Python through Homebrew in the first place.

Step 3 — Upgrade certifi and pip

certifi is the Python package that carries the list of trusted root certificates. Because authorities are added and retired over time, an old copy can miss the issuer a modern API uses. Upgrading it is quick, safe, and fixes a surprising share of cases on machines that have been running the same environment for a year or more.

python -m pip install --upgrade pip certifi
python -c "import certifi; print(certifi.where())"

Confirm the file is real and populated rather than an empty placeholder:

import certifi

path = certifi.where()
with open(path, "rb") as handle:
    data = handle.read()

print("Bundle:", path)
print("Size:", len(data), "bytes")
print("Certificates:", data.count(b"BEGIN CERTIFICATE"))

A healthy bundle is a few hundred kilobytes and holds well over a hundred certificates. A count of zero, or a FileNotFoundError, means the path Python thinks it should read does not exist — which is exactly what Step 2 repairs on macOS.

One subtlety worth knowing: pip itself uses the same trust machinery. If pip install fails with the same certificate error, you cannot upgrade certifi with pip to fix pip. Break the loop by fixing the operating system trust first (Step 2 on macOS, sudo apt install --reinstall ca-certificates on Debian and Ubuntu), or by passing the corporate certificate to pip directly as shown in the next step.

Step 4 — Point Python at a corporate root certificate

If only some hosts fail, a proxy or antivirus product on your network is performing TLS inspection: it terminates your encrypted connection, reads the traffic, then re-encrypts it with a certificate signed by your employer's own root authority. Browsers accept this because IT installed that root into the operating system. Python, reading certifi instead, has never heard of it.

Confirm the diagnosis by looking at who signed the certificate you are being served:

openssl s_client -showcerts -connect api.openai.com:443 </dev/null 2>/dev/null | grep "issuer="

If the issuer names your company, a security vendor, or your antivirus product rather than a public authority, that is your answer. Ask IT for the root certificate as a .pem file — it is not secret, and they hand it out routinely. Save it somewhere stable, then set two environment variables:

export SSL_CERT_FILE="$HOME/certs/corp-root-ca.pem"
export REQUESTS_CA_BUNDLE="$HOME/certs/corp-root-ca.pem"
export PIP_CERT="$HOME/certs/corp-root-ca.pem"

Add those three lines to ~/.zshrc or ~/.bashrc so they survive a new terminal. On Windows, use setx SSL_CERT_FILE "C:\certs\corp-root-ca.pem" in Command Prompt and open a fresh window.

Some libraries pin certifi internally and ignore those variables, so the most reliable approach in code is to build the trust settings yourself and hand them to the client. Because the openai package is built on httpx, you can pass a configured HTTP client straight into it:

import os
import ssl
import certifi
import httpx
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()  # reads .env, which is listed in .gitignore

ca_file = os.getenv("SSL_CERT_FILE", certifi.where())
context = ssl.create_default_context(cafile=ca_file)

http_client = httpx.Client(verify=context, timeout=30.0)
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), http_client=http_client)

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Reply with the single word: connected."}],
)
print(response.choices[0].message.content)

Verification is still fully on here. You have only widened the set of issuers Python trusts to include the one your employer legitimately operates. If this script now succeeds but the calls take a long time or drop midway, the remaining problem is the network rather than the certificates, and Fix Connection and Timeout Errors with AI APIs picks up from there.

Never ship verify=False

Every forum thread about this error contains someone suggesting verify=False, and it does make the message go away. It also makes the encryption meaningless. With verification disabled, Python accepts literally any certificate from anyone — a compromised café router, a misconfigured proxy, an attacker on the same office network. That party can present its own certificate, decrypt everything you send, and read the Authorization header where your API key sits in plain text. They can then bill your account until you notice.

It is not a temporary shortcut either. Disabled verification has a habit of surviving in a repository, being copied into the deployment script, and running in production for months. Treat it as a line that never gets committed, and read Manage API Keys Safely in Production for the wider habit this belongs to.

Disabling verification compared with fixing the trust store A three-row comparison matrix contrasting verify equals False with a proper certificate fix across certificate checking, exposure of the API key in transit, and whether the change is safe to ship to production. Concern verify=False The real fix Certificate chain is it verified? Not checked any cert accepted Checked fully issuer must match Your API key in transit Readable by any proxy in the path Only the real server can read it Ship it? production safety Never delete before merge Yes this is the norm
Disabling verification does not make the connection slightly less safe — it removes the only mechanism that proves you are talking to the API at all.

Certificate settings quick reference

These are the levers worth knowing, in the order you would reach for them.

SettingRead bySet it to
SSL_CERT_FILEPython's ssl module and most librariesAbsolute path to a PEM file
REQUESTS_CA_BUNDLErequests, and tools built on itThe same PEM path
PIP_CERTpip only, while installingThe same PEM path
ssl.create_default_context(cafile=...)Your own code, per clientPath chosen in Python

A PEM file is plain text: you can open it and see blocks starting with BEGIN CERTIFICATE. If yours does not, it is in a different format and needs converting before Python will read it.

Troubleshooting

  • certificate verify failed: unable to get local issuer certificate — Python found no trusted root for the chain. Run the macOS installer from Step 2, then upgrade certifi.
  • certificate verify failed: self signed certificate in certificate chain — a proxy or antivirus is re-signing your traffic. Add its root certificate and set SSL_CERT_FILE as in Step 4.
  • certificate verify failed: certificate has expired — usually the clock, not the certificate. Check the output of date, turn on automatic network time, and retry.
  • openai.APIConnectionError: Connection error. — the SDK wraps the real cause. Catch it and print exc.__cause__ to reveal the underlying SSLCertVerificationError; Read a Python Traceback in Five Minutes shows how to read the rest.
  • FileNotFoundError after setting SSL_CERT_FILE — the path is wrong or the variable is unquoted with a space in it. Run ls -l "$SSL_CERT_FILE" to check.

When to use this vs. alternatives

  • Fix the trust store when the whole machine is affected. Running the macOS installer or upgrading certifi repairs every script and every tool at once, which is the right level for a laptop you own.
  • Use an explicit SSLContext in code when only one project needs a special certificate. Building the context and passing it into httpx.Client keeps the change visible in the repository and portable to a colleague who reads it.
  • Use environment variables when a whole machine sits behind one proxy. They apply to pip, your editor's tooling, and every script without editing any of them, which is why IT departments hand out that instruction.
  • Never reach for verify=False. There is no situation in this list where it is the shorter path to a working, safe result.

Work the causes in the order this guide presents them and you will rarely need more than two attempts: the macOS installer clears most cases outright, a certifi upgrade clears most of the rest, and anything that survives both is a proxy that needs one certificate file and one environment variable. If a script imports fail rather than a connection fail, the sibling guide Fix ModuleNotFoundError: No Module Named openai is the one you want. Back to Debugging Python AI Errors.

Frequently asked questions

What does SSL: CERTIFICATE_VERIFY_FAILED actually mean in Python?

It means Python opened an encrypted connection, received the server's certificate, and could not trace that certificate back to an issuer it already trusts. The remote server is usually fine. The missing piece is on your machine: the list of trusted issuers Python reads is empty, out of date, or pointed at the wrong file.

Why does this error only happen on my Mac?

Python downloaded from python.org ships with its own trust list rather than borrowing the one in macOS Keychain, and that list stays empty until you run the Install Certificates.command file inside your Python folder. Running it once populates the list, and the error disappears for every script on that interpreter.

Is it safe to use verify=False to get past the error?

No. Turning verification off tells Python to accept any certificate at all, so anyone able to intercept your traffic can pose as the API, read the request, and copy your API key out of the header. It hides the fault instead of fixing it, and it must never reach a shared branch or a server.

How do I fix this on a corporate laptop with a security proxy?

Your employer's proxy re-signs every HTTPS connection with its own root certificate, which Python does not know. Ask IT for that root certificate as a PEM file, save it locally, and set the SSL_CERT_FILE and REQUESTS_CA_BUNDLE environment variables to its path. Verification then succeeds against the company issuer.

Can a wrong system clock cause a certificate error?

Yes. Every certificate carries a start and end date, and Python compares them against your computer's clock. If the clock is days or years off, a perfectly valid certificate reads as expired or not yet valid. Enable automatic network time, confirm the date, and retry before touching anything else.