---
title: "Verify vs Cert: The Python Requests Handbook"
description: "Understanding SSL/TLS in Python Requests: The 'verify' and 'cert' arguments explained with interactive animations."
pubDate: "2023-05-22"
lang: "en"
slug: "verify-and-cert-in-python-requests"
url: "https://hoangyell.com/verify-and-cert-in-python-requests/"
author: "HoangYell"
categories: ["Software Craft"]
tags: ["security", "software-craft"]
heroImage: "https://raw.githubusercontent.com/HoangGeek/store/main/webp/b1TbJXy.webp"
---

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

<style>:root\{--req-bg:#0f172a;--req-card:#1e293b;--req-accent:#3b82f6;--req-danger:#ef4444;--req-success:#10b981;--req-text:#e2e8f0;--req-sub:#94a3b8;\}.req-sim-container\{background:var(--req-bg);border:1px solid #334155;border-radius:12px;padding:24px;margin:2rem 0;font-family:'Inter',sans-serif;color:var(--req-text);position:relative;overflow:hidden;\}.req-sim-title\{font-size:0.9rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--req-sub);margin-bottom:20px;font-weight:700;\}.req-stage\{display:flex;justify-content:space-between;align-items:center;min-height:180px;position:relative;padding:0 40px;\}.req-entity\{width:80px;height:80px;border-radius:50%;background:var(--req-card);border:2px solid #475569;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:2;transition:all 0.3s cubic-bezier(0.175,0.885,0.32,1.275);position:relative;\}.req-icon\{font-size:2rem;margin-bottom:4px;\}.req-label\{position:absolute;bottom:-25px;font-size:0.75rem;white-space:nowrap;font-weight:600;\}.req-courier\{border-color:var(--req-accent);box-shadow:0 0 15px rgba(59,130,246,0.2);\}.req-bank\{border-color:var(--req-success);box-shadow:0 0 15px rgba(16,185,129,0.2);\}.req-hacker\{border-color:var(--req-danger);display:none;left:50%;top:50%;transform:translate(-50%,-50%);position:absolute;box-shadow:0 0 20px rgba(239,68,68,0.5);\}.req-line\{position:absolute;top:50%;left:120px;right:120px;height:4px;background:#334155;z-index:1;border-radius:2px;\}.req-packet\{width:24px;height:24px;background:#fbbf24;border-radius:4px;position:absolute;top:50%;left:80px;transform:translate(-50%,-50%);box-shadow:0 0 10px rgba(251,191,36,0.6);opacity:0;z-index:3;display:flex;align-items:center;justify-content:center;font-size:12px;\}.req-shield\{position:absolute;font-size:2rem;color:var(--req-accent);opacity:0;transition:all 0.3s;z-index:4;\}.req-controls\{margin-top:24px;display:flex;gap:12px;border-top:1px solid #334155;padding-top:16px;\}.req-btn\{background:var(--req-card);border:1px solid #475569;color:var(--req-text);padding:8px 16px;border-radius:6px;cursor:pointer;font-size:0.85rem;transition:all 0.2s;\}.req-btn:hover\{background:#334155;\}.req-btn.active\{background:var(--req-accent);border-color:var(--req-accent);color:white;\}.req-btn.danger.active\{background:var(--req-danger);border-color:var(--req-danger);\}.req-status\{margin-left:auto;font-family:monospace;font-size:0.85rem;color:var(--req-sub);align-self:center;\}</style><div class="req-sim-container" id="sim-verify"><div class="req-sim-title">Simulation: The Trust Check</div><div class="req-stage"><div class="req-line"></div><div class="req-entity req-courier"><div class="req-icon">🚚</div><div class="req-label">Requests</div><div class="req-shield" id="shield-verify">🛡️</div></div><div class="req-entity req-hacker" id="node-hacker"><div class="req-icon">🦹</div><div class="req-label">MITM</div></div><div class="req-entity req-bank" id="node-bank"><div class="req-icon">🏦</div><div class="req-label">Server</div></div><div class="req-packet" id="packet-verify">💰</div></div><div class="req-controls"><button class="req-btn active" onclick="setVerifyMode(true)" id="btn-verify-true">verify=True</button><button class="req-btn" onclick="setVerifyMode(false)" id="btn-verify-false">verify=False</button><button class="req-btn" style="margin-left:12px;border-color:var(--req-success)" onclick="runVerifySim()">▶ Send Request</button><div class="req-status" id="status-verify">Mode: Secure</div></div></div><script>let verifyMode=true;function setVerifyMode(isSecure)\{verifyMode=isSecure;document.getElementById('btn-verify-true').classList.toggle('active',isSecure);document.getElementById('btn-verify-false').classList.toggle('active',!isSecure);document.getElementById('btn-verify-false').classList.toggle('danger',!isSecure);const status=document.getElementById('status-verify');status.innerText=isSecure?"Mode: Secure (Checks Cert)":"Mode: Unsafe (Blind Trust)";status.style.color=isSecure?"var(--req-success)":"var(--req-danger)";document.getElementById('packet-verify').style.opacity=0;document.getElementById('node-hacker').style.display='none';document.getElementById('shield-verify').style.opacity=0;\}function runVerifySim()\{const packet=document.getElementById('packet-verify');const hacker=document.getElementById('node-hacker');const shield=document.getElementById('shield-verify');const status=document.getElementById('status-verify');packet.style.transition='none';packet.style.left='80px';packet.style.opacity='1';hacker.style.display='none';shield.style.opacity='0';void packet.offsetWidth;packet.style.transition='left 1.5s cubic-bezier(0.4, 0, 0.2, 1)';if(verifyMode)\{status.innerText="Checking Cert...";shield.style.opacity='1';setTimeout(()=>\{status.innerText="Cert Valid. Sending...";packet.style.left='calc(100% - 80px)';setTimeout(()=>\{status.innerText="Success: 200 OK";packet.style.opacity=0;shield.style.opacity=0;\},1500);\},500);\}else\{status.innerText="No Check. Sending Blindly...";hacker.style.display='flex';packet.style.left='50%';setTimeout(()=>\{status.innerText="INTERCEPTED!";status.style.color="var(--req-danger)";packet.style.transform="translate(-50%, -50%) scale(0)";setTimeout(()=>\{status.innerText="Data Stolen.";\},1000);\},750);\}\}</script>

### 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).

```python
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.

```python
# 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

<div class="req-sim-container" id="sim-cert"><div class="req-sim-title">Simulation: Mutual TLS (mTLS)</div><div class="req-stage"><div class="req-line"></div><div class="req-entity req-courier" id="mtls-courier"><div class="req-icon">🚚</div><div class="req-label">Client</div><div class="req-packet" id="packet-mtls" style="display:none;top:-30px;">💳</div></div><div class="req-entity req-bank" id="mtls-bank"><div class="req-icon">🏦</div><div class="req-label">Server</div></div></div><div class="req-controls"><button class="req-btn" id="btn-mtls" onclick="runMtlsSim()">▶ connect(cert=...)</button><div class="req-status" id="status-mtls">Waiting...</div></div></div><script>function runMtlsSim()\{const c=document.getElementById('mtls-courier');const b=document.getElementById('mtls-bank');const badge=document.getElementById('packet-mtls');const status=document.getElementById('status-mtls');status.innerText="1. Client Hello";badge.style.display='block';badge.innerText="💳";badge.style.left='50%';badge.style.opacity='0';b.style.transform='scale(1.1)';b.style.borderColor='#fbbf24';setTimeout(()=>\{status.innerText="2. Server: 'Who are you?'";setTimeout(()=>\{status.innerText="3. Client presents Cert...";badge.style.opacity='1';badge.style.transition='top 0.5s, opacity 0.5s';badge.style.top='-50px';setTimeout(()=>\{badge.style.transition='left 0.8s ease-in';badge.style.left='calc(100% - 40px)';setTimeout(()=>\{status.innerText="4. Server: 'Access Granted'";badge.style.opacity='0';b.style.borderColor='#10b981';b.style.boxShadow='0 0 20px #10b981';setTimeout(()=>\{b.style.transform='scale(1)';b.style.borderColor='#10b981';b.style.boxShadow='none';status.innerText="Connection Established.";\},1500);\},800);\},600);\},800);\},800);\}</script>

### How to use it
You need two files from your security team: a **Certificate** (public ID) and a **Key** (secret password).

```python
# 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

| Parameter | Analogy | Who is being checked? | Why use it? |
| :--- | :--- | :--- | :--- |
| **`verify`** | The ID Check | **The Server** | To ensure you aren't sending data to a hacker. |
| **`cert`** | The Badge | **The 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?*
