Python Requests vs Urllib: Simplifying HTTP Requests
While Python includes the built-in urllib package for
handling HTTP operations, the third-party requests library
significantly simplifies HTTP request dispatching through a
human-centric API. This article examines the core differences between
the two, demonstrating how requests eliminates verbose
boilerplate code for handling query parameters, submitting JSON
payloads, managing sessions, and processing server responses.
Simplified Syntax and URL Encoding
In urllib, constructing a basic GET request with query
parameters requires manual encoding and string concatenation. Developers
must import urllib.parse to encode dictionaries into query
strings and append them to the target URL before passing it to
urllib.request.urlopen().
The requests library automates this entire process.
Passing a dictionary to the params argument in
requests.get() automatically formats and encodes the query
string:
# Using urllib
import urllib.parse
import urllib.request
params = urllib.parse.urlencode({"query": "python", "page": 1})
url = f"https://httpbin.org/get?{params}"
with urllib.request.urlopen(url) as response:
html = response.read()
# Using requests
import requests
response = requests.get(
"https://httpbin.org/get", params={"query": "python", "page": 1}
)Native JSON Serialization and Parsing
Modern web APIs rely heavily on JSON, an area where the standard
library requires multi-step manual handling. To dispatch a JSON payload
using urllib, the developer must import the
json module, serialize the dictionary, encode the resulting
string to raw bytes, and manually define the
Content-Type: application/json header. Parsing the response
requires reading raw bytes, decoding them to UTF-8, and deserializing
them with json.loads().
The requests library abstracts these steps entirely.
Supplying a dictionary to the json keyword argument
automatically serializes the data and sets the proper headers. Decoding
JSON responses is achieved via the built-in .json()
method:
# Using urllib
import json
import urllib.request
data = json.dumps({"name": "Alice"}).encode("utf-8")
req = urllib.request.Request(
"https://httpbin.org/post",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as response:
result = json.loads(response.read().decode("utf-8"))
# Using requests
import requests
response = requests.post("https://httpbin.org/post", json={"name": "Alice"})
result = response.json()Intuitive Response Handling and Error Flow
urllib raises an HTTPError exception for
any non-200 HTTP status code, such as 404 Not Found or
500 Internal Server Error. This forces developers to wrap
standard HTTP dispatch calls in try/except blocks merely to
read the body or status code of an unsuccessful response.
By contrast, requests treats all valid HTTP responses
consistently. The response object exposes properties like
status_code, text, content, and
the boolean ok (which returns True for codes
under 400). If an exception is desired for bad statuses, calling
response.raise_for_status() provides that behavior
explicitly.
Session Management and Persistent State
Maintaining state across requests—such as cookies and connection
reuse—is complex in urllib. It requires configuring custom
handler objects, registering a cookie jar, and creating a custom opener
with urllib.request.build_opener().
requests simplifies connection pooling and state
persistence through the Session object. A session
automatically persists cookies across requests, reuses underlying TCP
connections via urllib3, and retains shared headers:
# Persistent session with requests
with requests.Session() as session:
session.headers.update({"Authorization": "Bearer token123"})
session.get("https://httpbin.org/cookies/set/sessioncookie/12345")
response = session.get("https://httpbin.org/cookies")Built-in Authentication Handling
Handling HTTP Basic Authentication in urllib demands
instantiating an HTTPPasswordMgrWithDefaultRealm, creating
an HTTPBasicAuthHandler, and building a specialized opener.
With requests, standard authentication types are passed as
simple tuples directly into the request via the auth
parameter (auth=('username', 'password')), removing the
need for custom handler plumbing.