← Back to Course Index
Lesson 14 of 20

HTTP Requests

~20 min · requests, httpx, sessions, auth, error handling, Pydantic parsing

Ref
Primary Source
requests — Official Docs

The most widely used Python HTTP library. Simple, batteries-included. Also see httpx for async HTTP.

1 — fetch vs requests: The Map

JavaScript — fetch
// GET
const res = await fetch("https://api.github.com/users/torvalds");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data.name);

// POST with JSON body
const res = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Alice" }),
});

// With auth
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${token}` }
});
Python — requests
import requests

# GET — raises on non-2xx with .raise_for_status()
res = requests.get("https://api.github.com/users/torvalds")
res.raise_for_status()   # raises HTTPError if 4xx/5xx
data = res.json()
print(data["name"])

# POST with JSON body
res = requests.post("/api/users", json={"name": "Alice"})

# With auth (Bearer token)
res = requests.get(url, headers={"Authorization": f"Bearer {token}"})

# Query params
res = requests.get("/search", params={"q": "python", "page": 2})
# → /search?q=python&page=2
# Install
pip install requests

2 — Response Object

res = requests.get("https://api.github.com/users/torvalds")

# Status
res.status_code       # 200
res.ok                # True if 200–299
res.raise_for_status()  # raises requests.HTTPError if not ok

# Body
res.json()            # parse JSON → dict/list
res.text              # raw string body
res.content           # raw bytes (for binary files)

# Headers
res.headers["Content-Type"]      # "application/json; charset=utf-8"
res.headers.get("X-RateLimit-Remaining")

# URL after redirects
res.url               # final URL

# Cookies
res.cookies["session_id"]

3 — All HTTP Methods

import requests

requests.get(url, params={...})
requests.post(url, json={...})          # JSON body
requests.post(url, data={...})          # form-encoded body
requests.post(url, files={"file": open("report.pdf", "rb")})  # multipart
requests.put(url, json={...})
requests.patch(url, json={...})
requests.delete(url)
requests.head(url)
requests.options(url)

4 — Sessions — Persist Headers & Cookies

A Session persists cookies, headers, and connection pooling across requests — like Axios instances in JS.

import requests

# Without session — creates new connection each time
for user_id in range(100):
    requests.get(f"/api/users/{user_id}", headers={"Authorization": f"Bearer {token}"})

# With session — reuses connection, keeps auth header
with requests.Session() as session:
    session.headers.update({
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
    })
    session.base_url = "https://api.example.com"   # not built-in, just a pattern

    for user_id in range(100):
        res = session.get(f"https://api.example.com/users/{user_id}")
        res.raise_for_status()
        print(res.json()["name"])

5 — Timeouts & Error Handling

import requests
from requests.exceptions import (
    HTTPError, ConnectionError, Timeout, RequestException
)

def fetch_user(user_id: int) -> dict:
    try:
        res = requests.get(
            f"https://api.example.com/users/{user_id}",
            timeout=(3.05, 10)  # (connect timeout, read timeout) in seconds
        )
        res.raise_for_status()
        return res.json()
    except Timeout:
        raise RuntimeError(f"Request timed out for user {user_id}")
    except HTTPError as e:
        if e.response.status_code == 404:
            return None
        raise
    except ConnectionError:
        raise RuntimeError("No internet connection")
    except RequestException as e:
        raise RuntimeError(f"Request failed: {e}")

# Always set a timeout — never leave it as None (default = wait forever!)
⚠ Always set a timeout

By default, requests will wait forever if the server doesn't respond. Always pass timeout=. A tuple (connect, read) gives you fine-grained control.

6 — httpx — Async HTTP

Use httpx when you're in an async context (Lesson 10). The API is nearly identical to requests.

import httpx
import asyncio

async def fetch_users(user_ids: list[int]) -> list[dict]:
    async with httpx.AsyncClient(
        base_url="https://api.example.com",
        headers={"Authorization": f"Bearer {token}"},
        timeout=10.0,
    ) as client:
        tasks = [client.get(f"/users/{uid}") for uid in user_ids]
        responses = await asyncio.gather(*tasks)
        return [r.raise_for_status().json() for r in responses]

# httpx also has a sync client — drop-in for requests
with httpx.Client(base_url="https://api.example.com") as client:
    res = client.get("/users/1")
    print(res.json())
💡 requests vs httpx

Use requests for sync scripts. Use httpx when you need async HTTP or HTTP/2 support. Both have almost identical APIs so switching is trivial.

7 — Parsing Responses with Pydantic

Validate and type API responses with Pydantic — like Zod for Python.

pip install pydantic
from pydantic import BaseModel, HttpUrl
import requests

class GitHubUser(BaseModel):
    login: str
    name: str | None = None
    public_repos: int
    followers: int
    html_url: HttpUrl

def get_github_user(username: str) -> GitHubUser:
    res = requests.get(f"https://api.github.com/users/{username}", timeout=5)
    res.raise_for_status()
    return GitHubUser(**res.json())  # validates and parses!

user = get_github_user("torvalds")
print(user.name)          # "Linus Torvalds"
print(user.public_repos)  # 7 (typed as int)
print(type(user.html_url))  # Url object, not raw string

🧠 Quiz

1. What does res.raise_for_status() do?

2. How do you pass query parameters with requests.get()?

3. When should you use httpx instead of requests?

4. What is the danger of not setting a timeout in requests?

0/4

Questions answered correctly.