Chapter 2: What Happens When You Order? Request & Response Models
In Chapter 1: The Simplest Way - The Functional API, we saw how easy it is to fetch a webpage or send data using simple functions like requests.get()
and requests.post()
. We also noticed that these functions return something called a Response
object.
But what exactly is that Response
object? And what happens behind the scenes when requests
sends your request? Just like ordering food involves more than just shouting your order and getting a meal, web requests have structured steps and data carriers. Understanding these helps you use requests
more effectively.
Why Models? The Need for Structure
Imagine ordering takeout again. You don’t just tell the restaurant “food!”; you give them specific details: “One large pepperoni pizza, delivery to 123 Main St.” The restaurant then prepares exactly that and delivers it back to you with a receipt.
Web requests work similarly. You need to tell the server:
- What you want (the URL, like
/get
or/post
). - How you want to interact (the method, like
GET
orPOST
). - Any extra details (like headers or data you’re sending).
The server then replies with:
- If it worked (a status code, like
200 OK
or404 Not Found
). - Information about the reply (headers, like the content type).
- The actual stuff you asked for (the content, like HTML or JSON).
Requests
uses special Python objects to hold all this information in an organized way. These are the Request and Response Models.
The Main Characters: Request, PreparedRequest, and Response
Think of the process like ordering at a restaurant:
Request
Object (Your Order Slip): This is your initial intention. It holds the basic details of the request you want to make: the URL, the method (GET
,POST
, etc.), any headers you want to add, and any data you want to send. You usually don’t create this object directly when using the simple functional API, butrequests
does it for you internally.- Analogy: You write down “Large Pizza, Pepperoni, Extra Cheese” on an order slip.
PreparedRequest
Object (The Prepared Tray): This is the finalized, ready-to-go version of your request.Requests
takes the initialRequest
object, processes it (encodes data, applies cookies, adds default headers likeUser-Agent
), and gets it ready to be sent over the network. It contains the exact bytes and final details. This is mostly an internal step.- Analogy: The kitchen takes your slip, makes the pizza, puts it in a box, adds napkins and maybe a drink, and puts it all on a tray ready for the delivery driver.
Response
Object (The Delivered Meal): This object represents the server’s reply after thePreparedRequest
has been sent and the server has responded. It contains everything the server sent back: the status code (Did the order succeed?), the response headers (What kind of food is this? How was it packaged?), and the actual content (The pizza itself!). This is the object you usually work with directly.- Analogy: The delivery driver hands you the tray with the pizza and receipt. You check the receipt (
status_code
,headers
) and eat the pizza (content
).
- Analogy: The delivery driver hands you the tray with the pizza and receipt. You check the receipt (
Most of the time, you’ll interact primarily with the Response
object. But knowing about Request
and PreparedRequest
helps understand what requests
is doing for you.
Working with the Response
Object
Let’s revisit our requests.get()
example from Chapter 1 and see what useful things are inside the response
object we get back.
import requests
url = 'https://httpbin.org/get'
print(f"Fetching data from: {url}")
response = requests.get(url)
# --- Exploring the Response Object ---
# 1. Status Code: Was it successful?
print(f"\nStatus Code: {response.status_code}") # A number like 200 (OK) or 404 (Not Found)
print(f"Was it successful (status < 400)? {response.ok}") # A boolean True/False
# 2. Response Headers: Information *about* the response
print(f"\nResponse Headers (Content-Type): {response.headers['Content-Type']}")
# Headers are like a dictionary (Case-Insensitive)
print("All Headers:")
for key, value in response.headers.items():
print(f" {key}: {value}")
# 3. Response Content (Body): The actual data!
# - As text (decoded using guessed encoding):
print("\nResponse Text (first 100 chars):")
print(response.text[:100])
# - As raw bytes (useful for non-text like images):
print("\nResponse Content (bytes, first 20):")
print(response.content[:20])
# 4. JSON Helper: If the content is JSON
json_url = 'https://httpbin.org/json'
print(f"\nFetching JSON from: {json_url}")
json_response = requests.get(json_url)
if json_response.ok and 'application/json' in json_response.headers.get('Content-Type', ''):
try:
data = json_response.json() # Decodes JSON into a Python dict/list
print("Decoded JSON data:")
print(data)
print(f"Value of 'title': {data['slideshow']['title']}")
except requests.exceptions.JSONDecodeError:
print("Response was not valid JSON.")
What we learned from the Response
:
response.status_code
: A standard HTTP status code number.200
means “OK”.404
means “Not Found”. Many others exist.response.ok
: A quick boolean check.True
if the status code is less than 400 (meaning success or redirect),False
for errors (4xx or 5xx codes).response.headers
: A dictionary-like object holding the response headers sent by the server (likeContent-Type
,Date
,Server
). It’s case-insensitive, soresponse.headers['content-type']
works too.response.text
: The response body decoded into a string.Requests
tries to guess the correct text encoding based on headers, or falls back to a guess based on the content itself. Good for HTML, plain text, etc.response.content
: The response body as raw bytes, exactly as received from the server. Use this for images, downloads, or when you need precise control over decoding.response.json()
: A convenient method that tries to parse theresponse.text
as JSON and returns a Python dictionary or list. It raises an error if the content isn’t valid JSON.
The Response
object neatly packages all the server’s reply information for you to use.
How It Works Internally: From Request to Response
When you call requests.get(url)
, the following happens under the hood (simplified):
- Create
Request
:Requests
creates aRequest
object containing the method ('GET'
), theurl
, and any other arguments you provided (likeheaders
orparams
). (Seerequests/sessions.py
request
method which creates amodels.Request
) - Prepare
Request
: ThisRequest
object is then passed to a preparation step. Here, it becomes aPreparedRequest
. This involves:- Merging session-level settings (like default headers or cookies from a Session, which the functional API uses temporarily).
- Encoding parameters (
params
). - Encoding the body (
data
orjson
). - Handling authentication (
auth
). - Adding standard headers (like
User-Agent
,Accept-Encoding
). - Resolving the final URL. (See
requests/sessions.py
prepare_request
method which callsPreparedRequest.prepare
inrequests/models.py
)
- Send
PreparedRequest
: ThePreparedRequest
, now containing the exact bytes and headers, is handed off to a Transport Adapter (we’ll cover these in Transport Adapters). The adapter handles the actual network communication (opening connections, sending bytes, dealing with HTTP/HTTPS specifics). (Seerequests/sessions.py
send
method which callsadapter.send
inrequests/adapters.py
) - Receive Reply: The Transport Adapter waits for the server’s reply (status line, headers, body).
- Build
Response
: The adapter takes the raw reply data and uses it to build theResponse
object you receive. It parses the status code, headers, and makes the raw content available. (Seerequests/adapters.py
build_response
method which creates amodels.Response
) - Return
Response
: Thesend
method returns the fully formedResponse
object back to your code.
Here’s a diagram showing the journey:
sequenceDiagram
participant UserCode as Your Code (e.g., requests.get)
participant Session as requests Session (Temporary or Explicit)
participant PrepReq as PreparedRequest
participant Adapter as Transport Adapter
participant Server as Web Server
participant Resp as Response
UserCode->>Session: Call get(url) / post(url, data=...)
Session->>Session: Create models.Request object
Session->>PrepReq: prepare_request(request) -> PreparedRequest
Note over PrepReq: Encodes data, adds headers, cookies etc.
Session->>Adapter: send(prepared_request)
Adapter->>Server: Send HTTP Request bytes
Server-->>Adapter: Send HTTP Response bytes
Adapter->>Resp: build_response(raw_reply) -> Response
Resp-->>Adapter: Return Response
Adapter-->>Session: Return Response
Session-->>UserCode: Return Response
You can see the definitions for these objects in requests/models.py
:
# File: requests/models.py (Highly Simplified)
class Request:
"""A user-created Request object. Used to prepare a PreparedRequest."""
def __init__(self, method=None, url=None, headers=None, files=None,
data=None, params=None, auth=None, cookies=None, hooks=None, json=None):
self.method = method
self.url = url
# ... other attributes ...
def prepare(self):
"""Constructs a PreparedRequest for transmission."""
p = PreparedRequest()
p.prepare(
method=self.method,
url=self.url,
# ... pass other attributes ...
)
return p
class PreparedRequest:
"""The fully mutable PreparedRequest object, containing the exact bytes
that will be sent to the server."""
def __init__(self):
self.method = None
self.url = None
self.headers = None
self.body = None
# ... other attributes ...
def prepare(self, method=None, url=None, headers=None, files=None, data=None,
params=None, auth=None, cookies=None, hooks=None, json=None):
"""Prepares the entire request."""
# ... Logic to encode data, set headers, handle auth, etc. ...
self.method = method
self.url = # processed url
self.headers = # final headers
self.body = # encoded body bytes or stream
# ...
class Response:
"""Contains a server's response to an HTTP request."""
def __init__(self):
self._content = False # Content hasn't been read yet
self.status_code = None
self.headers = CaseInsensitiveDict() # Special dictionary for headers
self.raw = None # The raw stream from the network connection
self.url = None
self.encoding = None
self.history = [] # List of redirects
self.reason = None # Text reason, e.g., "OK"
self.cookies = cookiejar_from_dict({})
self.elapsed = datetime.timedelta(0) # Time taken
self.request = None # The PreparedRequest that led to this response
@property
def content(self):
"""Content of the response, in bytes."""
# ... logic to read from self.raw if not already read ...
return self._content
@property
def text(self):
"""Content of the response, in unicode."""
# ... logic to decode self.content using self.encoding or guessed encoding ...
return decoded_string
def json(self, **kwargs):
"""Returns the json-encoded content of a response, if any."""
# ... logic to parse self.text as JSON ...
return python_object
# ... other properties like .ok, .is_redirect, and methods like .raise_for_status() ...
Understanding these models gives you a clearer picture of how requests
turns your simple function call into a network operation and packages the result neatly for you.
Conclusion
You’ve learned about the core data carriers in Requests
:
Request
: Your initial intent.PreparedRequest
: The finalized request ready for sending.Response
: The server’s reply, containing status, headers, and content.
While you mostly interact with the Response
object after making a request, knowing about the Request
and PreparedRequest
helps demystify the process. You saw how to access useful attributes of the Response
like status_code
, headers
, text
, content
, and the handy json()
method.
In Chapter 1, we noted that the functional API creates a temporary setup for each request. This is simple but inefficient if you need to talk to the same website multiple times, perhaps needing to maintain login status or custom settings. How can we do that better?
Next: Chapter 3: Remembering Things - The Session Object
Generated by AI Codebase Knowledge Builder