Fix: Python SSL: CERTIFICATE_VERIFY_FAILED
Part of: Python Errors
Quick Answer
How to fix Python SSL CERTIFICATE_VERIFY_FAILED error caused by missing root certificates on macOS, expired system certs, corporate proxies, and self-signed certificates in requests, urllib, and httpx.
The Error
You run a Python script that makes an HTTPS request and get:
ssl.SSLCertificateError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)Or through the requests library:
requests.exceptions.SSLError: HTTPSConnectionPool(host='api.example.com', port=443):
Max retries exceeded with url: /data (Caused by SSLError(SSLCertificateError("bad handshake:
Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')])")))Or via urllib:
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1129)>Or after installing Python on macOS:
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:852)Note: This error is distinct from the pip-specific SSL error (pip install fails). This article covers SSL errors when Python scripts make HTTPS requests at runtime. For pip install SSL failures, see Fix: pip SSL Certificate Verify Failed.
Why This Happens
When Python makes an HTTPS connection, it verifies the server’s SSL certificate against a set of trusted root certificates (a “CA bundle”). The error means Python could not find a trusted root certificate to verify the server’s certificate chain.
Common causes:
- macOS Python installation: The official Python installer from python.org does NOT use macOS’s system certificate store. It ships with no CA bundle configured, causing all HTTPS requests to fail until you run a certificate install script.
- Corporate proxy / firewall: Your company’s network intercepts HTTPS traffic and presents its own certificate, which Python does not trust.
- Self-signed or private CA certificates: Connecting to an internal server with a self-signed cert or one issued by a private CA.
- Outdated system CA bundle: The CA bundle bundled with Python or OpenSSL is outdated and missing newer root certificates.
- Virtual environment isolation: A virtualenv uses different certificate paths than the system Python.
- Docker containers: Minimal base images often have no CA certificates installed.
The most dangerous thing you can do here is add verify=False. That suppresses the error and turns every HTTPS request into an unauthenticated TLS handshake, meaning a man-in-the-middle on any network you touch can read and modify your traffic. The error is the security model working correctly. Your job is to fix the trust chain, not to disable the check. Every Stack Overflow answer that says “just use verify=False” is teaching a security antipattern that production codebases inherit and never remove.
The other historical wrinkle worth knowing is the 2021 DST Root CA X3 expiration. Let’s Encrypt’s chain was anchored at DST Root CA X3, which expired in September 2021. Older OpenSSL builds (1.0.x and many 1.1.0 builds) did not handle the cross-signed replacement correctly, and Python interpreters built against those OpenSSL versions started failing to verify Let’s Encrypt sites on that date. If you are debugging an old Python (3.6 or earlier) on a long-lived server, the fix may simply be “upgrade Python.” Python 3.10 also tightened hostname checks and dropped some legacy flags, so a script that worked on 3.8 can fail on 3.10 against a server with a sloppy certificate. The error message is the same — the underlying validator changed.
Diagnostic Timeline
Use this sequence instead of skipping straight to verify=False.
Minute 0 — Confirm the platform. If you are on macOS and just installed Python from python.org, the fix is almost certainly running Install Certificates.command. Skip the rest of the timeline and try that first.
Minute 2 — Identify which CA store Python is reading. Run python -c "import ssl; print(ssl.get_default_verify_paths())". The output shows cafile, capath, and openssl_capath. If cafile is None and the paths point at locations that do not exist, your interpreter has no CA store and every HTTPS call will fail.
Minute 4 — Confirm certifi is installed and locate its bundle. Run python -m certifi. This prints the path to the bundled Mozilla CA file. If certifi is missing, pip install certifi. Set SSL_CERT_FILE=$(python -m certifi) and retry the script — if it works, the fix is wiring certifi into the environment permanently.
Minute 7 — Reproduce the handshake with the openssl CLI. Run openssl s_client -connect api.example.com:443 -showcerts < /dev/null. The output shows every certificate the server presented, plus the verification status. Look for Verify return code: 21 (unable to verify the first certificate) — that means the server is not sending its intermediate certificate. The fix there is on the server, not in your code.
Minute 10 — Test against a known-good site. Run python -c "import urllib.request; print(urllib.request.urlopen('https://www.google.com').status)". If this also fails, the problem is your entire CA bundle, not the specific endpoint. If only your target fails, the issue is server-side or related to a private CA.
Minute 13 — Inspect for a corporate MITM. Run openssl s_client -connect api.example.com:443 < /dev/null 2>&1 | grep issuer. If the issuer is your employer (“Zscaler”, “BlueCoat”, “Forcepoint”, “Netskope”, “Cisco Umbrella”), you are behind a TLS-intercepting proxy. The fix is to add the corporate root CA to REQUESTS_CA_BUNDLE — never to bypass with verify=False.
Minute 16 — Check the date. Run date. If your system clock is more than a few minutes off, certificate notBefore/notAfter checks fail and every site looks expired. sudo ntpdate pool.ntp.org (or restart the time-sync service) and retry.
Minute 18 — Print the actual chain Python sees. Use the snippet at the bottom of this article (ssl.SSLContext.wrap_socket) to print the server’s cert. Compare the issuer field against your CA bundle. If the issuer is not in certifi.where(), you have isolated the missing root.
Fix 1: Run the Certificate Install Script (macOS)
This is the most common cause on macOS. The official Python.org installer ships with an Install Certificates.command script that installs the certifi CA bundle.
Open Finder and navigate to:
/Applications/Python 3.x/Double-click Install Certificates.command. Or run it from the terminal:
/Applications/Python\ 3.11/Install\ Certificates.commandReplace 3.11 with your Python version. This runs:
pip install --upgrade certifi
/Applications/Python\ 3.11/python3 -m certifiAfter running this, retry your script. This fix resolves the error for the vast majority of macOS users.
Why this matters: Python on macOS uses its own bundled OpenSSL rather than the system’s Security framework. The system’s certificate store (used by Safari, curl, etc.) is not accessible to Python by default. The
certifipackage provides an up-to-date Mozilla CA bundle that Python can use.
Fix 2: Use certifi Explicitly in Your Code
If you cannot run the install script (e.g., on a server), point Python to the certifi CA bundle explicitly:
With requests:
import requests
import certifi
response = requests.get("https://api.example.com/data", verify=certifi.where())
print(response.json())With urllib:
import urllib.request
import ssl
import certifi
context = ssl.create_default_context(cafile=certifi.where())
with urllib.request.urlopen("https://api.example.com/data", context=context) as response:
print(response.read())Set it globally for all requests in a session:
import requests
import certifi
import os
# Set the environment variable for the entire process
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
os.environ["SSL_CERT_FILE"] = certifi.where()Install certifi if you do not have it:
pip install certifiFix 3: Fix Corporate Proxy / Man-in-the-Middle Certificates
If your company uses a proxy that intercepts HTTPS traffic, Python sees the proxy’s certificate instead of the server’s. Python does not trust the proxy’s self-signed or corporate CA certificate.
Option A: Add the corporate certificate to your trusted CA bundle:
Get the corporate root certificate file (ask your IT department — it is usually a .pem or .crt file). Then:
import requests
response = requests.get(
"https://internal.company.com/api",
verify="/path/to/corporate-root-ca.pem"
)Or combine it with certifi’s bundle:
import certifi
import shutil
import os
# Append corporate cert to certifi's bundle
corporate_cert = "/path/to/corporate-root-ca.pem"
certifi_bundle = certifi.where()
# Create a combined bundle
combined_bundle = "/tmp/combined-ca-bundle.pem"
shutil.copy(certifi_bundle, combined_bundle)
with open(corporate_cert, "r") as corp, open(combined_bundle, "a") as bundle:
bundle.write(corp.read())
os.environ["REQUESTS_CA_BUNDLE"] = combined_bundleOption B: Set the certificate via environment variable:
export REQUESTS_CA_BUNDLE=/path/to/corporate-root-ca.pem
export SSL_CERT_FILE=/path/to/corporate-root-ca.pem
python your_script.pyFix 4: Fix Self-Signed Certificates for Internal Servers
If you are connecting to a server with a self-signed certificate (common in development environments):
Pass the certificate file directly:
import requests
# Verify against the server's self-signed cert
response = requests.get(
"https://localhost:8443/api",
verify="/path/to/server-cert.pem"
)For mutual TLS (client certificate authentication):
response = requests.get(
"https://internal-api.company.com/data",
verify="/path/to/ca-bundle.pem",
cert=("/path/to/client-cert.pem", "/path/to/client-key.pem")
)Common Mistake: Using
verify=Falseto bypass SSL verification. This disables certificate checking entirely, making your connection vulnerable to man-in-the-middle attacks. Never useverify=Falsein production code. It suppresses the error but does not fix it.
If you must use verify=False in development (not recommended), suppress the InsecureRequestWarning:
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get("https://localhost:8443/api", verify=False)Fix 5: Update Certificates on Linux / Docker
On Debian/Ubuntu-based systems:
sudo apt-get update && sudo apt-get install -y ca-certificates
sudo update-ca-certificatesOn Alpine Linux (common in Docker):
apk add --no-cache ca-certificates
update-ca-certificatesIn a Dockerfile:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Or install certifi and set the env var
RUN pip install certifi
ENV SSL_CERT_FILE=/usr/local/lib/python3.11/site-packages/certifi/cacert.pemFix 6: Fix SSL in Virtual Environments
Virtual environments sometimes do not inherit the system’s certificate configuration:
# Activate your virtualenv
source venv/bin/activate
# Install certifi inside the virtualenv
pip install certifi
# Check which cert file Python is using
python -c "import ssl; print(ssl.get_default_verify_paths())"Set the cert path for the virtualenv:
export SSL_CERT_FILE=$(python -m certifi)
export REQUESTS_CA_BUNDLE=$(python -m certifi)Add these exports to your .env file or shell profile to make them persistent. For .env loading issues, see Fix: .env variables not loading.
Fix 7: Fix httpx and Other HTTP Libraries
The fix applies similarly to other HTTP libraries:
httpx:
import httpx
import certifi
import ssl
ssl_context = ssl.create_default_context(cafile=certifi.where())
with httpx.Client(verify=ssl_context) as client:
response = client.get("https://api.example.com/data")aiohttp:
import aiohttp
import ssl
import certifi
async def fetch():
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com/data", ssl=ssl_context) as response:
return await response.json()boto3 (AWS SDK):
export AWS_CA_BUNDLE=/path/to/corporate-ca.pemOr in code:
import boto3
session = boto3.Session()
client = session.client("s3", verify="/path/to/ca-bundle.pem")Diagnose the Certificate Chain
Before applying a fix, identify exactly which certificate is failing:
import ssl
import socket
hostname = "api.example.com"
port = 443
context = ssl.create_default_context()
try:
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
print("Certificate is valid")
print("Subject:", cert["subject"])
print("Issuer:", cert["issuer"])
print("Expires:", cert["notAfter"])
except ssl.SSLCertificateError as e:
print("SSL Error:", e)Or use OpenSSL from the command line:
openssl s_client -connect api.example.com:443 -showcertsThis shows the full certificate chain and which CA issued the certificate, helping you determine whether to update certifi, add a corporate cert, or contact the server admin.
Still Not Working?
Check if the site itself has a certificate issue. Use an online SSL checker (e.g., SSL Labs) to verify the server’s certificate is valid and properly chained. If the server is misconfigured, the fix is on the server side.
Check for clock skew. SSL certificates have expiration dates. If your system clock is significantly off, certificates may appear expired or not yet valid. Sync your system clock: sudo ntpdate pool.ntp.org.
Check Python’s OpenSSL version. Older OpenSSL versions bundled with Python may not support newer TLS extensions:
python -c "import ssl; print(ssl.OPENSSL_VERSION)"If you see OpenSSL 1.0.x, upgrade Python to a version that ships with OpenSSL 1.1.x or 3.x.
Check for SNI issues. Some servers require SNI (Server Name Indication) to present the correct certificate. Python 3.x supports SNI by default. If you are on Python 2 (end of life), the pyOpenSSL and ndg-httpsclient packages add SNI support.
Look for the server skipping intermediate certificates. Many servers misconfigure their chain and send only the leaf certificate. Browsers paper over this with AIA fetching; Python does not. Run openssl s_client -connect host:443 -showcerts — if you see only one certificate in the output, the server admin needs to bundle the intermediate. As a workaround, download the intermediate from the issuing CA and add it to your local bundle.
Rule out a stale pip install certifi from years ago. certifi ships a snapshot of Mozilla’s CA list. A two-year-old version is missing the Let’s Encrypt ISRG Root X2 and newer roots. Run pip install --upgrade certifi and re-export SSL_CERT_FILE=$(python -m certifi).
Check for virtualenv Python pointing at a deleted system Python. A venv’s python is a symlink to the system Python that created it. If the system Python was uninstalled or moved, the venv’s SSL module loads but ssl.get_default_verify_paths() returns nonsense. Recreate the venv with the current system Python.
Verify requests is not picking up a stale REQUESTS_CA_BUNDLE from your shell. Run env | grep -E "(SSL|CA_BUNDLE|CERT)" and remove any leftover exports from previous troubleshooting sessions before testing the fix.
Check for proxy environment variables conflicting with CA settings. HTTPS_PROXY and https_proxy route traffic through an intermediate that may itself terminate TLS. If both are set and the proxy is broken, the symptom looks like a cert error. Unset them temporarily and retry.
For connection errors that happen before SSL negotiation, see Fix: Python ConnectionError: Max Retries Exceeded.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Python requests.get() Hanging — Timeout Not Working
How to fix Python requests hanging forever — why requests.get() ignores timeout, how to set connect and read timeouts correctly, use session-level timeouts, and handle timeout exceptions properly.
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.
Fix: Pipenv Not Working — Lock File Generation, Shell Activation, and Dependency Resolution
How to fix Pipenv errors — pipenv lock takes forever, Pipfile.lock not generated, shell activation broken, no virtualenv created, dependency conflict, and migration to uv or Poetry.