Python Requests: Ignore SSL Certificate Errors, and the Right Way

Rewritten as a tested reference. Every call shown below was executed by the example package against a local HTTPS server with a self-signed certificate; the output blocks are what it printed (see "How the outputs were captured"). Four recipes from the 2024 version were wrong and are shown failing here: passing an ssl.SSLContext as verify=, REQUESTS_CA_BUNDLE="", "verify=False disables verification globally", and the "certificate pinning" example built on the same invalid verify= call.
You called requests.get() and got this:
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
The two-line way to make it go away:
import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get("https://localhost:8443/", verify=False)
And the one-line way to fix it instead of hiding it, when you have the server's certificate or your company's CA file:
response = requests.get("https://localhost:8443/", verify="certs/localhost.pem")
The rest of this page shows what each option does, with the real output, including the recipes you will find elsewhere that do nothing or crash in current requests.
Video Tutorial
The setup, and how the outputs were captured
The example package starts https://localhost:8443/ with a certificate it generates itself (openssl req -x509 … -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"). Nothing trusts that certificate, so a default request fails exactly as above, and the certificate is only valid for the name localhost, which matters for the hostname-mismatch case later. A few requests go to a public page with a valid certificate to show the default path working. All commands assume the package directory as the working directory, where certs/localhost.pem lives.
The package runs every call through a small helper (show() in _common.py) that prints the return value, or the exception's type and message instead of raising, so one failing line does not stop the script; it also redirects warnings to stdout so they appear next to the result they belong to. The code blocks below show the calls as bare expressions; the output blocks show what the helper printed, with its labels removed. In a file of your own, wrap a call in print() to see its value. If you paste a block into a plain Python file, a failing line raises a normal traceback ending in the same SSLError: … text, and warnings go to stderr with a file:line: prefix.
Option 1: verify=False for one request
import requests
requests.get("https://localhost:8443/", verify=False).status_code
requests.get("https://localhost:8443/", verify=False).status_code # again
requests.get("https://localhost:8443/").status_code # next call, no verify=
InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
200
InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
200
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
The request succeeds and urllib3 warns once for each unverified request. verify=False is per request: the third call, without it, fails as before. That is the first thing the 2024 version of this article got wrong. (On requests older than 2.32.0 there was a real leak: the first verify=False request on a Session switched verification off for later requests to the same origin, CVE-2024-35195. If you are pinned below 2.32.0, upgrade before relying on per-request verify.)
Option 2: silence the warning
import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
requests.get("https://localhost:8443/", verify=False).status_code
200
disable_warnings calls warnings.simplefilter("ignore", …) for that category, so it is process-wide. Call it once at import time in scripts where you have decided to skip verification; in a library, do not call it at all, the warning belongs to whoever runs the code.
Option 3: Session.verify = False for every request in a session
The supported way to switch verification off for many requests is a Session:
import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
s = requests.Session()
s.verify = False
s.get("https://localhost:8443/").status_code
s.get("https://localhost:8443/index.html").status_code
s.get("https://localhost:8443/", verify=True).status_code
200
200
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
The third line shows the precedence rule: a verify argument on the call wins over the session setting, so verify=True on one request restores verification for that request. requests has no module-level verify setting: requests.get() and friends build a fresh Session for every call (requests/api.py), so if your code base calls them from many places, route those calls through one module-level Session (or a thin wrapper) and set verify there.
Option 4: trust the certificate instead (the fix)
If the server is yours, or your company's TLS-intercepting proxy re-signs every site, you have a certificate you can trust explicitly. Pass its path as verify:
import requests
requests.get("https://localhost:8443/", verify="certs/localhost.pem").status_code
200
No warning. verify accepts a path to a PEM file or to a directory of certificates prepared with c_rehash. For a corporate proxy, the file is the proxy's root CA, exported from the machine's trust store; for a self-signed server, the server certificate itself works, as here.
The certificate is valid but the name is not
Same certificate, same server, addressed by IP instead of by name:
requests.get("https://127.0.0.1:8443/", verify="certs/localhost.pem").status_code
requests.exceptions.SSLError: HTTPSConnectionPool(host='127.0.0.1', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: IP address mismatch, certificate is not valid for '127.0.0.1'. (_ssl.c:1010)")))
Read the message: IP address mismatch means the chain is trusted and the name you used is not in the certificate's Subject Alternative Names. The fix is to use the name the certificate was issued for, or to reissue the certificate with the right SAN entries. verify=False also "fixes" this, by skipping the check that just told you something useful.
One consequence to know before you put this in a config file: the path replaces the default certifi bundle for that request, it does not add to it. The same file against a public site:
requests.get("https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html", verify="certs/localhost.pem").status_code
requests.exceptions.SSLError: HTTPSConnectionPool(host='scrapingant.github.io', port=443): Max retries exceeded with url: /scrapingant-examples/fixtures/dynamic-delayed.html (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1010)')))
If the same client also talks to the public internet (the corporate-proxy case, where the proxy may not intercept every host), build one bundle from certifi's file plus your CA, and point verify at that:
cat "$(python -m certifi)" certs/localhost.pem > certs/bundle.pem
requests.get("https://localhost:8443/", verify="certs/bundle.pem").status_code
requests.get("https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html", verify="certs/bundle.pem").status_code
200
200
Option 5: environment variables (REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE)
When you cannot change the code, point requests at your CA file from the environment. Both variables work; REQUESTS_CA_BUNDLE is checked first. env_check.py is a small script in the package that makes one default requests.get() and prints the result:
REQUESTS_CA_BUNDLE=certs/localhost.pem python env_check.py
CURL_CA_BUNDLE=certs/localhost.pem python env_check.py
200
200
The bundle-replacement rule from option 4 applies here too: with REQUESTS_CA_BUNDLE set, every request in that process trusts only that file, so use the combined bundle for anything that also reaches public sites.
Now the recipes that circulate for "disable verification via an environment variable", run as-is:
REQUESTS_CA_BUNDLE='' python env_check.py
REQUESTS_CA_BUNDLE=/nonexistent.pem python env_check.py
PYTHONHTTPSVERIFY=0 python env_check.py
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
OSError: Could not find a suitable TLS CA certificate bundle, invalid path: /nonexistent.pem
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
An empty REQUESTS_CA_BUNDLE is ignored, because Session.merge_environment_settings reads it as os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("CURL_CA_BUNDLE") or verify, and an empty string is false, so the default certifi bundle is used. A path that does not exist raises OSError before any connection. PYTHONHTTPSVERIFY=0 is a Python 2.7 setting (PEP 493) and does nothing here. Those two bundle variables are the only environment variables that function reads for TLS; the environment can choose which CA file is trusted, it cannot turn verification off.
Two more details from the same function: the environment is consulted only when verify is True or None (an explicit path in code wins), and only when Session.trust_env is true. With trust_env = False the variable is ignored and the request fails again:
REQUESTS_CA_BUNDLE=certs/localhost.pem python env_check.py --no-trust-env
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
Option 6: a custom SSLContext, the way that actually works
The 2024 version of this page said you could pass an ssl.SSLContext as verify=. You cannot:
import ssl, requests
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
requests.get("https://localhost:8443/", verify=ctx)
TypeError: stat: path should be string, bytes, os.PathLike or integer, not SSLContext
The 2024 "certificate pinning" example, ssl.create_default_context(cafile="certs/localhost.pem") passed the same way, fails with the same TypeError. verify is a boolean or a path. A context goes into the transport adapter, which is also how you scope trust to one host prefix:
import ssl, requests
from requests.adapters import HTTPAdapter
class SSLContextAdapter(HTTPAdapter):
def __init__(self, ssl_context, **kw):
self.ssl_context = ssl_context
super().__init__(**kw)
def init_poolmanager(self, *a, **kw):
kw["ssl_context"] = self.ssl_context
return super().init_poolmanager(*a, **kw)
ctx = ssl.create_default_context(cafile="certs/localhost.pem")
s = requests.Session()
s.mount("https://localhost:8443", SSLContextAdapter(ctx))
s.get("https://localhost:8443/").status_code # our CA, hostname checked
s.get("https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html").status_code # default adapter
s.get("https://127.0.0.1:8443/").status_code # not mounted -> default verification
200
200
requests.exceptions.SSLError: HTTPSConnectionPool(host='127.0.0.1', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
The mounted prefix gets the custom trust, every other URL keeps the default certifi verification. This is the "trust one internal CA without touching anything else" recipe, and the cleaner alternative to the combined bundle from option 4.
What does not work is putting CERT_NONE in that context to disable verification for one host. requests builds the connection pool with cert_reqs="CERT_REQUIRED" unless verify is False (adapters.py, _urllib3_request_context), and urllib3 applies that value to the context you supplied (connection.py, context.verify_mode = resolve_cert_reqs(cert_reqs)):
requests.exceptions.SSLError: HTTPSConnectionPool(host='127.0.0.1', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
To skip verification for one host only, force verify=False in the adapter instead:
class NoVerifyAdapter(HTTPAdapter):
def send(self, request, **kwargs):
kwargs["verify"] = False
return super().send(request, **kwargs)
s = requests.Session()
s.mount("https://127.0.0.1:8443", NoVerifyAdapter())
s.get("https://127.0.0.1:8443/").status_code # unverified (warning printed)
s.get("https://localhost:8443/").status_code # different prefix -> still verified
InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
200
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
The same thing in urllib and urllib3
Standard library urllib.request:
import ssl, urllib.request
urllib.request.urlopen("https://localhost:8443/").status # default
urllib.request.urlopen("https://localhost:8443/", context=ssl._create_unverified_context()).status # off
urllib.request.urlopen("https://localhost:8443/", context=ssl.create_default_context(cafile="certs/localhost.pem")).status # trust the CA
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)>
200
200
ssl._create_unverified_context() has an underscore, and it is still the spelling PEP 476 gives for code that must opt out of verification.
urllib3 directly:
import urllib3
urllib3.PoolManager().request("GET", "https://localhost:8443/").status # default
urllib3.PoolManager(cert_reqs="CERT_NONE").request("GET", "https://localhost:8443/").status # off
urllib3.PoolManager(ca_certs="certs/localhost.pem").request("GET", "https://localhost:8443/").status # trust the CA
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='localhost', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1010)')))
InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
200
200
For httpx, verify=False switches verification off and verify=ssl.create_default_context(cafile="certs/localhost.pem") trusts your CA; httpx takes the context object that requests rejects, and since httpx 0.28 a string path is deprecated. The two clients are compared in Requests vs HTTPX; the same error in curl and wget is covered in curl-ignore-ssl and wget-ignore-ssl.
CERTIFICATE_VERIFY_FAILED on a site that is fine in the browser
If the error appears on a public site with a valid certificate, the server is not the problem; the trust store your Python uses is. requests does not use the operating system's store, it ships its own through the certifi package:
import certifi, requests
requests.get("https://scrapingant.github.io/scrapingant-examples/fixtures/dynamic-delayed.html").status_code
certifi.where()
200
.../site-packages/certifi/cacert.pem
Three causes, in the order to check them:
- Stale
certifi. The requests documentation's own advice is to upgrade certifi frequently;pip install --upgrade certifiand retry. - macOS python.org installer. Its ReadMe states that the certificates in the system keychain are not used as defaults by the Python
sslmodule, and shipsInstall Certificates.commandto install certifi's bundle for the standard library; run it once from the Python application folder.requestsalready uses certifi, so it is theurllibcase this fixes. - A corporate proxy that re-signs TLS. Every site then presents your company's CA, which is in the OS store and not in certifi. Export that CA and use the combined bundle from option 4 with
REQUESTS_CA_BUNDLE, or use the OS store: on Python 3.10 or later,pip install truststoreand calltruststore.inject_into_ssl()at the start of your application (not from a library), which makes thesslmodule verify against the system store;pipitself has used the system store together with certifi by default since pip 24.2 on those Python versions. The last two are documented, not run by the package.
When ScrapingAnt is not needed, and when it is
None of the above needs a scraping service. An internal or development server: trust its certificate (option 4). A corporate proxy: add its CA (option 5). A public site that fails only in Python: update certifi. Disabling verification is for throwaway scripts against hosts you control; in anything that handles credentials or data you care about, an unverified connection means anyone on the path can read and alter it.
Where the target site, not your client, is the obstacle (pages that need JavaScript rendering, or your IP range being blocked), the ScrapingAnt API changes the shape of the problem. When you call the ScrapingAnt API, your code makes one TLS connection, to api.scrapingant.com; the target page is fetched by ScrapingAnt's infrastructure, so the target's certificate is not something your client verifies.
Limitations
- The examples run against a local server; a real TLS-intercepting proxy was described from documentation, not exercised.
- Trust-store changes (
Install Certificates.command,truststore) are described, not run, because they modify the machine. - Everything was measured on requests 2.34.2 and urllib3 2.8.0; the source lines quoted are from those versions.
Examples tested on 2026-09-16 with Python 3.12.11, requests 2.34.2, urllib3 2.8.0, certifi 2026.07.22. Code: scrapingant-examples/examples/requests-ignore-ssl.
This article was drafted with AI assistance from a tested evidence packet and reviewed by the named author, who is responsible for the code, measurements and corrections.