SSLCertVerificationError: certificate verify failed: unable to get local issuer certificate
1 · The lesson
readWhat this error means
The connection was made and the server presented a certificate, but Python could not build a chain of trust from that certificate up to a root authority it recognises. It is refusing to continue because it cannot prove the server is who it claims to be.
This is TLS working correctly. The certificate is not necessarily bad — your machine simply cannot verify it.
When you see it
Traceback (most recent call last): File "fetch.py", line 4, in <module> r = requests.get("https://internal.corp.example/api") requests.exceptions.SSLError: HTTPSConnectionPool(host='internal.corp.example', port=443): Max retries exceeded with url: /api (Caused by SSLError( SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)')))
A different last line points at a different cause:
certificate has expired— the certificate is genuinely out of date.Hostname mismatch— the certificate is valid, but not for the name you asked for.self signed certificate— nothing signed it but itself.
Why it happens
- A corporate proxy is intercepting TLS. It re-signs every connection with its own root, which your browser trusts because IT installed it in the OS store — and which Python does not, because Python uses its own bundle from
certifi. - The server sends an incomplete chain. It presents its leaf certificate but omits the intermediate, so there is nothing to link it to a root. Browsers often paper over this by fetching the missing intermediate; Python does not.
- The root store is old or missing — a slim container image with no
ca-certificatespackage installed. - macOS installs from python.org ship without running
Install Certificates.command, socertifiis never wired up.
How to fix it
See what the server actually sends:
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null
Look for Verify return code. unable to get local issuer certificate there too means the chain really is incomplete, and the fix belongs on the server.
In a container, install the roots:
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*On macOS, after a python.org install:
/Applications/Python\ 3.13/Install\ Certificates.command
Behind a corporate proxy, trust that proxy's root explicitly — do not disable verification:
import requests r = requests.get(url, verify="/etc/ssl/certs/corp-root.pem")
setup added so this can run · defines url
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) url = _AutoMock('url')
Or for everything at once, via the environment:
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/corp-root.pem export SSL_CERT_FILE=/etc/ssl/certs/corp-root.pem
What not to do. verify=False makes the error disappear and makes the connection worthless: any machine on the path can present its own certificate and read or alter the traffic. It is the most-copied answer to this error and it turns an encrypted connection into a decorated one. If you use it to unblock yourself locally, it must not reach a branch you deploy.
# not this — you have kept the padlock icon and thrown away what it means requests.get(url, verify=False)
setup added so this can run · defines url, requests
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) url = _AutoMock('url') requests = _AutoMock('requests')
When you'd actually see this in real code
- Code that works on a laptop and fails in CI, because the laptop trusts the corporate proxy root and the CI runner does not.
- A slim base image (
python:3.13-slim, Alpine) with noca-certificates— every HTTPS call fails identically. - An internal service whose certificate was renewed but whose intermediate was not re-deployed with it.
- A certificate that expired overnight, taking a scheduled job with it, with nothing else changed.
Related errors
- TimeoutError / requests.exceptions.ReadTimeout — connected but silent, rather than rejected at the handshake.
- [ConnectionRefusedError: [Errno 111] Connection refused](../error-connection-refused/) — never got as far as TLS.
See Also
- All Python errors — the full index, by type and by when it happens.
- APIs & HTTP
- Auth & Security