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 just 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 really is out of date.Hostname mismatch: the certificate is valid, but not for the name you asked for.self signed certificate: the certificate was signed by itself, not by an authority.
Why it happens
- A corporate proxy is intercepting TLS. It re-signs every connection with its own root. Your browser trusts that root because IT installed it in the OS store.
requestsdoes not trust it, becauserequestsuses its own bundle fromcertifi. - 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 hide this problem by fetching the missing intermediate themselves, but Python does not.
- The root store is old or missing, for example in a slim container image with no
ca-certificatespackage installed. - macOS installs from python.org ship without running
Install Certificates.command, socertifiis never set up.
How to fix it
Check what the server really 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, but it also makes the connection unsafe: any machine on the path can present its own certificate and read or change the traffic. It is the most-copied answer to this error. The traffic is still encrypted, but you no longer know who is on the other end. If you use it to unblock yourself locally, it must not reach a branch you deploy.
# not this: you keep the padlock icon but throw 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 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, where every HTTPS call fails in the same way. - 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: the connection was made but the server never answered, instead of being rejected at the handshake.
- [ConnectionRefusedError: [Errno 111] Connection refused](../error-connection-refused/): the connection 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
Found this useful? Share it with a friend who’s learning Python.