The error, verbatim
Jump to the fix ↓requests.exceptions.SSLError: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/
(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1144)')))
Tested on
- Python
- 3.12.15, 3.13.17, 3.14.5, 3.14.9, 3.15.1, 3.15.2
- Libraries
- requests 2.34.2, certifi 2026.10.23, truststore, pip 26.1.1 and 26.2.2
- OpenSSL
- 3.0.20 and 3.5.9 (Windows), 3.5.5 (Linux)
- OS
- Windows 11 Pro with Avast HTTPS scanning; Ubuntu on GitHub Actions
Contents
CERTIFICATE_VERIFY_FAILED means Python couldn’t trace the server’s certificate back to a root it trusts. The tail of the message tells you why, and the two I reproduced need different fixes:
| Message ends with | Cause | Go to |
|---|---|---|
unable to get local issuer certificate |
Something between you and the server re-signs HTTPS (antivirus, a company proxy), and Python doesn’t trust its root | 1 |
Basic Constraints of CA cert not marked critical |
You do trust that root, but it’s built in a way Python 3.13+ refuses | 2 |
1. “unable to get local issuer certificate”: antivirus or a proxy
My Windows laptop runs Avast, which scans HTTPS by re-signing every site’s certificate with its own root, “Avast Web/Mail Shield Root”. Python saw that as pypi.org’s issuer:
{'organizationalUnitName': 'generated by Avast Antivirus for SSL/TLS scanning', 'organizationName': 'Avast Web/Mail Shield', 'commonName': 'Avast Web/Mail Shield Root'}
Avast adds its root to the Windows certificate store, so browsers are fine. What Python does depends on the library. On Python 3.14.5 and 3.15.2, in fresh virtual environments:
| Library | Result |
|---|---|
urllib.request (built in) |
✅ worked |
pip install |
✅ worked |
requests |
❌ certificate verify failed: unable to get local issuer certificate |
The built-in ssl module on Windows also loads the Windows certificate store, and pip uses that store through truststore. requests trusts only the list in the certifi package, which has never heard of Avast. That’s why pip can install requests, and requests then fails on the same site.
Have requests (and everything else) use the operating system’s certificate store:
pip install truststoreimport truststore
truststore.inject_into_ssl()
import requests
requests.get("https://pypi.org/simple/")If you can’t change the code, point requests at a bundle file that contains the extra root:
set REQUESTS_CA_BUNDLE=C:\path\to\bundle.pemDon’t turn verification off (verify=False) to get past this. It hides the error and also hides a real interception, which is exactly what this check exists to catch.
2. “Basic Constraints of CA cert not marked critical”: Python 3.13 and newer
This one arrives with an upgrade. Your code trusts a company or proxy root, it worked on Python 3.12, and on 3.13 it fails:
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Basic Constraints of CA cert not marked critical (_ssl.c:1032)>
Python 3.13 turned on a stricter certificate check by default, the VERIFY_X509_STRICT flag. A CA certificate is supposed to mark its “Basic Constraints” field as critical, and a lot of in-house roots don’t. To test it, I made a root without that marking, signed a local HTTPS server with it, and told Python to trust the root:
| Python | Trust the root with cafile= / verify= / SSL_CERT_FILE |
Root in the system store |
|---|---|---|
| 3.12.15 | ✅ | ✅ |
| 3.13.17 | ❌ not marked critical | ❌ not marked critical |
| 3.14.9 | ❌ not marked critical | ❌ not marked critical |
| 3.15.1 | ❌ not marked critical | ❌ not marked critical |
The root itself is fine for 3.12; only the stricter check rejects it. truststore didn’t help here on Linux: with the root in the system store, it failed on 3.13 and newer with the same message.
The real fix is a root that’s built correctly. If it’s your company’s, ask whoever runs the proxy to reissue it with basicConstraints marked critical. Until then, clear the strict flag for the connections that need that root, and only those:
import ssl
ctx = ssl.create_default_context(cafile="company-root.pem")
ctx.verify_flags &= ~ssl.VERIFY_X509_STRICTFor requests, pass that context through an adapter:
import ssl, requests
from requests.adapters import HTTPAdapter
class CompanyRoot(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = ssl.create_default_context(cafile="company-root.pem")
ctx.verify_flags &= ~ssl.VERIFY_X509_STRICT
kwargs["ssl_context"] = ctx
return super().init_poolmanager(*args, **kwargs)
session = requests.Session()
session.mount("https://", CompanyRoot())The behind-Avast case in section 1 is not this one. Avast’s root passed the strict check: with the bundle, urllib worked on 3.14 and 3.15 with the flag still on.
How this was tested
October 31, 2026. Section 1: Windows 11 Pro with Avast’s Web Shield scanning HTTPS, Python 3.14.5 (OpenSSL 3.0.20) and 3.15.2 (OpenSSL 3.5.9), each in a new virtual environment with no certificate environment variables set, requesting https://pypi.org/simple/. Section 2: GitHub Actions ubuntu-latest with Python 3.12.15, 3.13.17, 3.14.9 and 3.15.1: an OpenSSL-generated root with basicConstraints=CA:TRUE not marked critical, a server certificate signed by it for localhost, and Python’s own HTTPS server, then the same checks, and again after adding the root with update-ca-certificates. The scripts and the output are public.
— N.K., end of entry No.094