PythonMastery
reference 4 min read · lesson 34 of 45 in Errors

SSLCertVerificationError: certificate verify failed: unable to get local issuer certificate

1 · The lesson

read

What 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

python
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-certificates package installed.
  • macOS installs from python.org ship without running Install Certificates.command, so certifi is never wired up.

How to fix it

See what the server actually sends:

bash
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:

dockerfile
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:

bash
/Applications/Python\ 3.13/Install\ Certificates.command

Behind a corporate proxy, trust that proxy's root explicitly — do not disable verification:

python
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:

bash
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.

python
# 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 no ca-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.

See Also