When you need fine-grained control over HTTP requests, Python's http.client module gives you direct access to the protocol layer.

Basic GET Request

import http.client
 
conn = http.client.HTTPSConnection("api.example.com")
conn.request("GET", "/users/1")
 
response = conn.getresponse()
print(response.status, response.reason)  # 200 OK
 
data = response.read()
conn.close()

POST with JSON Body

import http.client
import json
 
conn = http.client.HTTPSConnection("api.example.com")
 
payload = json.dumps({"name": "Owen", "role": "engineer"})
headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer token123"
}
 
conn.request("POST", "/users", body=payload, headers=headers)
 
response = conn.getresponse()
result = json.loads(response.read())
conn.close()

Connection Reuse

Keep connections open for multiple requests:

import http.client
 
conn = http.client.HTTPSConnection("api.example.com")
 
# Multiple requests on same connection
for user_id in [1, 2, 3]:
    conn.request("GET", f"/users/{user_id}")
    response = conn.getresponse()
    data = response.read()  # Must read before next request
    print(f"User {user_id}: {response.status}")
 
conn.close()

Critical: Always read the response body before making another request on the same connection.

Handling Timeouts

import http.client
import socket
 
conn = http.client.HTTPSConnection("api.example.com", timeout=5)
 
try:
    conn.request("GET", "/slow-endpoint")
    response = conn.getresponse()
except socket.timeout:
    print("Request timed out")
finally:
    conn.close()

Context Manager Pattern

import http.client
from contextlib import contextmanager
 
@contextmanager
def http_connection(host, port=443, timeout=10):
    conn = http.client.HTTPSConnection(host, port, timeout=timeout)
    try:
        yield conn
    finally:
        conn.close()
 
# Usage
with http_connection("api.example.com") as conn:
    conn.request("GET", "/health")
    response = conn.getresponse()
    print(response.read().decode())

Reading Response Headers

import http.client
 
conn = http.client.HTTPSConnection("example.com")
conn.request("GET", "/")
 
response = conn.getresponse()
 
# All headers as list of tuples
print(response.getheaders())
 
# Specific header
content_type = response.getheader("Content-Type")
cache_control = response.getheader("Cache-Control", "no-cache")
 
conn.close()

Chunked Transfer Encoding

Handle streaming responses:

import http.client
 
conn = http.client.HTTPSConnection("api.example.com")
conn.request("GET", "/stream")
 
response = conn.getresponse()
 
# Check if chunked
if response.getheader("Transfer-Encoding") == "chunked":
    while True:
        chunk = response.read(1024)
        if not chunk:
            break
        process_chunk(chunk)
else:
    data = response.read()
 
conn.close()

Custom HTTP Methods

import http.client
 
conn = http.client.HTTPSConnection("api.example.com")
 
# PATCH request
conn.request("PATCH", "/users/1", 
             body='{"status": "active"}',
             headers={"Content-Type": "application/json"})
 
# DELETE request
conn.request("DELETE", "/users/1")
 
# OPTIONS request
conn.request("OPTIONS", "/users")
response = conn.getresponse()
allowed = response.getheader("Allow")  # "GET, POST, OPTIONS"

Debugging with set_debuglevel

import http.client
 
conn = http.client.HTTPSConnection("api.example.com")
conn.set_debuglevel(1)  # Print request/response details
 
conn.request("GET", "/users/1")
response = conn.getresponse()
# Prints: send/reply headers, etc.

HTTP vs HTTPS

import http.client
 
# HTTP (port 80, no encryption)
http_conn = http.client.HTTPConnection("example.com")
 
# HTTPS (port 443, TLS encryption)
https_conn = http.client.HTTPSConnection("example.com")
 
# Custom port
custom_conn = http.client.HTTPSConnection("api.example.com", port=8443)

When to Use http.client

Use http.client when you need:

  • No dependencies: Part of standard library
  • Protocol-level control: Custom headers, methods, connection management
  • Connection pooling: Manual connection reuse
  • Debugging: Low-level visibility into HTTP traffic

For most use cases, requests or httpx are more convenient. But http.client is invaluable when you need to go lower or avoid external dependencies.

Real-World Example: API Client

import http.client
import json
from typing import Any
 
class APIClient:
    def __init__(self, host: str, token: str):
        self.host = host
        self.token = token
        self.conn = None
    
    def connect(self):
        self.conn = http.client.HTTPSConnection(self.host)
    
    def close(self):
        if self.conn:
            self.conn.close()
    
    def request(self, method: str, path: str, 
                body: dict = None) -> dict:
        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }
        
        payload = json.dumps(body) if body else None
        self.conn.request(method, path, body=payload, headers=headers)
        
        response = self.conn.getresponse()
        data = response.read()
        
        if response.status >= 400:
            raise Exception(f"API error: {response.status}")
        
        return json.loads(data) if data else {}
 
# Usage
client = APIClient("api.example.com", "secret-token")
client.connect()
try:
    user = client.request("GET", "/users/1")
    client.request("PATCH", "/users/1", {"name": "Updated"})
finally:
    client.close()

The http.client module is Python's gateway to HTTP at the protocol level—essential knowledge for building robust network code.

React to this post: