Featured image of post Verify vs Cert: The Python Requests Handbook

Verify vs Cert: The Python Requests Handbook

Understanding SSL/TLS in Python Requests: The 'verify' and 'cert' arguments explained with interactive animations.

You’ve seen the error. You’ve felt the panic.

SSLError(CertificateError("hostname 'bankofamerica.com' doesn't match..."))

And you’ve probably done the “lazy fix”: adding verify=False just to make the red text go away.

Stop.

Before you disable security on your production app, let’s actually understand what verify and cert do. And because SSL handshakes are boring, we’re going to visualize them as a high-stakes money transfer.


The Analogy: The Armored Courier

Imagine requests is an Armored Courier (Client). The server (e.g., google.com) is the Bank (Server). The payload is a Bag of Cash (Data).

There are two critical questions that must be answered before any money changes hands:

  1. “Is this the Real Bank?” (Handled by verify)
  2. “Is this a Real Courier?” (Handled by cert)

1. verify: Checking the Bank

Who checks who? The Courier checks the Bank.
Default in Requests: True (Secure).

When you send a request, your code wants to know if it’s talking to the real server or an imposter.

The Mechanism

  1. Bank presents its ID Card (SSL Certificate).
  2. Courier checks its Book of Trusted IDs (CA Bundle).
  3. If the Bank’s ID is signed by someone in the Book? Trust.

Interactive Simulation: The Trust Check

Simulation: The Trust Check
🚚
Requests
🛡️
🦹
MITM
🏦
Server
💰
Mode: Secure

Scenario A: verify=True (The Good)

By default, requests acts like a strict bouncer. It grabs the digital ID bundle from certifi (a Python package containing trusted CAs like DigiCert, Let’s Encrypt).

1
2
3
4
import requests

# This is the default. You don't even need to type verify=True.
requests.get('https://github.com', verify=True) 

Scenario B: verify=False (The Ugly)

You turn off the ID check. The courier walks up to anyone wearing a “Bank” t-shirt and hands them the money.

This is fine for localhost testing.
This is suicide on public WiFi or the open internet.

1
2
3
# Disables the safety check. 
# Creates a massive security hole ("Man in the Middle" attack).
requests.get('https://internal-tool.local', verify=False)

Pro Tip: If you have a custom internal CA (common in corporate VPNs), don’t set False! Instead, point to your company’s certificate file: requests.get(url, verify='/path/to/company-ca.pem')


2. cert: The Badge Check

Who checks who? The Bank checks the Courier.
Also known as: mTLS (Mutual TLS) or Two-Way SSL.

Sometimes, the Bank is exclusive. It doesn’t talk to just anyone. It demands proof that you are an authorized courier.

The Mechanism

  1. Courier initiates connection.
  2. Bank says “Show me your badge.”
  3. Courier sends its Client Certificate (.crt) and Private Key (.key).
  4. Bank validates the badge. If valid? Access Granted.

Interactive Simulation: The Double Check

Simulation: Mutual TLS (mTLS)
🚚
Client
🏦
Server
Waiting...

How to use it

You need two files from your security team: a Certificate (public ID) and a Key (secret password).

1
2
3
4
5
6
7
# Tuple format: (cert_path, key_path)
client_cert = ('/path/to/client.crt', '/path/to/client.key')

requests.get(
    'https://secure-bank.com/api/vault', 
    cert=client_cert
)

Sometimes these are combined into a single .pem file. In that case, just pass the string path: cert='/path/to/combined.pem'.


TL;DR Summary

ParameterAnalogyWho is being checked?Why use it?
verifyThe ID CheckThe ServerTo ensure you aren’t sending data to a hacker.
certThe BadgeThe Client (You)To prove you have permission to access the server.

Next time requests throws an SSL error, don’t just blindly set verify=False. Ask yourself: Do I trust this bank?

Made with the laziness 🦥
by a busy guy

Subscribe to My Newsletter