HTTP Requests with Python: GET, POST, Headers, Parameters, and JSON
Data engineers and analysts frequently need to retrieve data from websites, APIs, and web services. Python’s requests library provides a convenient interface for communicating with these services through HTTP.
This article introduces:
- The basic structure of HTTP communication
- Sending GET and POST requests
- Adding query parameters
- Inspecting request and response headers
- Processing text, binary, and JSON responses
- Sending form data and JSON payloads
- Handling timeouts and HTTP errors
- Reusing connections with sessions
What Is HTTP?
HTTP, or Hypertext Transfer Protocol, defines how clients and servers exchange information over the web.
An HTTP interaction normally consists of two parts:
- A client sends an HTTP request.
- A server returns an HTTP response.
For example, a Python program might request weather data from an API. The API server processes the request and returns a JSON response containing the requested information.
An HTTP request can contain:
- A request method, such as GET or POST
- A URL
- Headers
- Query parameters
- An optional request body
The response can contain:
- A status code
- Response headers
- Text, JSON, binary data, or another response body
The Python Requests Library
Requests is a third-party Python library that provides a straightforward interface for sending HTTP requests.
It is not part of Python’s standard library. Install it with:
python -m pip install requestsThen import it into Python:
import requestsPython also includes standard-library HTTP tools such as urllib.request and http.client. Requests is often preferred because it provides a more concise and readable interface.
Sending a Basic GET Request
A GET request is normally used to retrieve a resource from a server.
import requests
url = "https://www.ibm.com/"
response = requests.get(url, timeout=10)
response.raise_for_status()
print(response.status_code)The requests.get() function sends the request and returns a Response object.
The timeout argument prevents the program from waiting indefinitely. Requests does not apply a timeout unless one is explicitly supplied.
The raise_for_status() method raises an exception when the server returns an unsuccessful HTTP status such as 404 Not Found or 500 Internal Server Error.
Understanding HTTP Status Codes
The response status code indicates the outcome of the request.
print(response.status_code)Common status-code groups include:
| Range | Meaning |
|---|---|
| 200–299 | Successful response |
| 300–399 | Redirection |
| 400–499 | Client-side error |
| 500–599 | Server-side error |
Some common individual codes are:
| Status code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 301 | Moved Permanently |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
| 503 | Service Unavailable |
A 200 response is common, but it is not the only successful status code.
Inspecting the Final URL
The response object records the final URL associated with the request:
print(response.url)This can be useful when the server redirects the request or when Requests adds encoded query parameters to the URL.
Request Headers and Response Headers
Request headers contain information sent from the client to the server.
print(response.request.headers)Response headers contain information returned by the server.
print(response.headers)The two should not be confused:
# Headers sent to the server
request_headers = response.request.headers
# Headers returned by the server
response_headers = response.headersRequests stores response headers in a case-insensitive dictionary-like object. For example:
content_type = response.headers.get("Content-Type")
print(content_type)A content type might look like:
text/html; charset=UTF-8or:
application/jsonThe Date response header, when present, generally represents the server’s response origination time. It does not represent the exact time at which the client sent the request.
Examining the Request Body
The outgoing request body is available through:
print(response.request.body)A conventional GET request generally does not include a body, so this will normally return:
None
Technically, HTTP does not make it impossible for a GET request to contain content. However, GET request content has no generally defined semantics and may be rejected by some systems. Data used to refine a GET request should normally be supplied through query parameters. This behavior is described in the current HTTP Semantics specification.
Reading a Text Response
When the response contains text, use the text attribute:
html = response.text
print(html[:200])Requests determines an encoding and decodes the response into a Python string.
The selected encoding can be inspected with:
print(response.encoding)If the server declares the wrong encoding, it can be changed before accessing response.text:
response.encoding = "utf-8"
html = response.textReading Binary Content
Use response.content when the response contains raw bytes:
binary_data = response.content
print(type(binary_data))This is appropriate for content such as:
- Images
- PDF documents
- ZIP archives
- Audio files
- Other non-text resources
The difference is:
response.text # Decoded Python string
response.content # Raw bytesSending Query Parameters with GET
APIs frequently use query parameters to filter or customize a response.
A URL containing query parameters might look like:
https://httpbin.org/get?name=Joseph&ID=123The query string begins with ?. Each parameter is represented as a name-value pair, and multiple pairs are separated with &.
Instead of constructing this string manually, pass a dictionary to the params argument:
import requests
url = "https://httpbin.org/get"
parameters = {
"name": "Joseph",
"ID": "123"
}
response = requests.get(
url,
params=parameters,
timeout=10
)
response.raise_for_status()
print(response.url)The resulting URL will resemble:
https://httpbin.org/get?name=Joseph&ID=123Requests automatically performs URL encoding. This is safer and more reliable than joining strings manually.
For example:
parameters = {
"search": "data engineering",
"page": 2
}
response = requests.get(
"https://httpbin.org/get",
params=parameters,
timeout=10
)
print(response.url)The space in data engineering is encoded appropriately.
The Requests documentation provides additional examples of passing parameters in URLs.
Processing a JSON Response
Many APIs return data in JSON format.
Use response.json() to parse the response into a Python object:
data = response.json()
print(type(data))
print(data)For the httpbin.org/get endpoint, query parameters are available under the args key:
print(data["args"])Expected result:
{
"ID": "123",
"name": "Joseph"
}Before assuming that a response contains JSON, it can be useful to inspect its content type:
content_type = response.headers.get("Content-Type", "")
if "application/json" in content_type:
data = response.json()However, the content type alone is not a guarantee that the body contains valid JSON. JSON parsing can still fail if the server returns malformed content.
Also note that successfully parsing JSON does not mean the HTTP request succeeded. Check the status with raise_for_status() first.
Sending a POST Request
POST requests are commonly used to submit data to a server.
Unlike a conventional GET request, POST data is usually placed in the request body.
Sending Form Data
Pass a dictionary to the data argument to send form-encoded data:
import requests
url = "https://httpbin.org/post"
payload = {
"name": "Joseph",
"ID": "123"
}
response = requests.post(
url,
data=payload,
timeout=10
)
response.raise_for_status()
print(response.url)
print(response.request.body)The URL normally does not contain the form values because they were placed in the request body.
The test service returns the submitted form data under the form key:
result = response.json()
print(result["form"])Expected result:
{
"ID": "123",
"name": "Joseph"
}Sending JSON Data with POST
Modern APIs frequently expect JSON rather than form-encoded data.
Use the json argument:
payload = {
"name": "Joseph",
"ID": 123
}
response = requests.post(
"https://httpbin.org/post",
json=payload,
timeout=10
)
response.raise_for_status()
result = response.json()
print(result["json"])Using json=payload tells Requests to:
- Serialize the Python object as JSON
- Place it in the request body
- Set an appropriate JSON content type
This is usually preferable to manually calling json.dumps() and setting the header yourself.
Form Data and JSON Are Different
The following requests do not send the same representation:
requests.post(url, data=payload, timeout=10)requests.post(url, json=payload, timeout=10)The first normally sends:
application/x-www-form-urlencodedThe second normally sends:
application/jsonUse the format required by the API documentation.
POST Requests Can Also Have Query Parameters
POST data is usually sent in the body, but a POST URL can still contain query parameters.
response = requests.post(
"https://httpbin.org/post",
params={"source": "tutorial"},
json={"name": "Joseph"},
timeout=10
)
response.raise_for_status()
print(response.url)This request has:
source=tutorialin the URL{"name": "Joseph"}in the request body
Therefore, GET versus POST should not be understood simply as “URL data versus body data.” Their primary difference is their HTTP semantics and intended use.
GET and POST Compared
| Feature | GET | POST |
|---|---|---|
| Typical purpose | Retrieve a resource | Submit or process data |
| Parameters | Commonly in the URL | Can be in the URL or body |
| Request body | Usually omitted | Commonly used |
| Safe operation | Intended to be safe | May change server state |
| Idempotent semantics | Yes | Not necessarily |
| Common API use | Search and retrieval | Creation, submission, actions |
A GET request should not normally change server data. Repeating a POST request, however, may create duplicate records or repeat an operation unless the API provides idempotency protection.
Adding Custom Headers
HTTP headers can specify the expected response format, identify the client, or provide authentication credentials.
headers = {
"Accept": "application/json",
"User-Agent": "DataSphere-Tutorial/1.0"
}
response = requests.get(
"https://httpbin.org/headers",
headers=headers,
timeout=10
)
response.raise_for_status()
print(response.json())An API token might be provided using an authorization header:
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}Do not hardcode production credentials directly into source code. Use environment variables or an appropriate secrets-management system.
Sensitive credentials should generally not be placed in URL query parameters because URLs can appear in logs, browser histories, analytics systems, and monitoring tools.
Handling Errors and Timeouts
Production code should account for network errors, timeouts, invalid responses, and unsuccessful HTTP status codes.
import requests
def fetch_json(url, params=None):
try:
response = requests.get(
url,
params=params,
timeout=(3.05, 15)
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
print("The request timed out.")
except requests.exceptions.HTTPError as error:
print(f"HTTP error: {error}")
except requests.exceptions.JSONDecodeError:
print("The response did not contain valid JSON.")
except requests.exceptions.RequestException as error:
print(f"Request failed: {error}")
return NoneThe timeout tuple represents:
timeout=(connection_timeout, read_timeout)In this example:
- Requests has approximately 3.05 seconds to establish a connection.
- It has 15 seconds to wait between response data operations.
A timeout is not necessarily a limit on the total duration of the complete request.
Reusing Connections with a Session
When making several requests to the same service, use a Session.
import requests
with requests.Session() as session:
session.headers.update({
"Accept": "application/json",
"User-Agent": "DataSphere-Tutorial/1.0"
})
first_response = session.get(
"https://httpbin.org/get",
params={"page": 1},
timeout=10
)
second_response = session.get(
"https://httpbin.org/get",
params={"page": 2},
timeout=10
)
first_response.raise_for_status()
second_response.raise_for_status()Sessions can provide:
- Connection pooling
- Shared headers
- Cookie persistence
- Shared authentication configuration
- Better performance for repeated requests
Additional configuration options are documented in the Requests guide to session objects.
A Reusable API Client Example
The following function combines several recommended practices:
import requests
def get_api_data(base_url, parameters=None):
headers = {
"Accept": "application/json",
"User-Agent": "DataSphere-Client/1.0"
}
try:
response = requests.get(
base_url,
params=parameters,
headers=headers,
timeout=(3.05, 15)
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
raise RuntimeError("The API request timed out.")
except requests.exceptions.HTTPError as error:
raise RuntimeError(
f"The API returned an HTTP error: {error}"
) from error
except requests.exceptions.JSONDecodeError as error:
raise RuntimeError(
"The API returned invalid JSON."
) from error
except requests.exceptions.RequestException as error:
raise RuntimeError(
f"The API request failed: {error}"
) from error
parameters = {
"name": "Joseph",
"ID": "123"
}
result = get_api_data(
"https://httpbin.org/get",
parameters
)
print(result["args"])The httpbin.org service is useful for demonstrations and testing. Sensitive or confidential information should not be sent to a public testing service.
Practical Reliability and Security Guidelines
When using HTTP requests in a data pipeline:
- Use HTTPS whenever possible.
- Always specify a timeout.
- Call
raise_for_status()before processing the response. - Validate the expected content type.
- Handle JSON parsing errors.
- Do not place secrets in query parameters.
- Do not hardcode API keys in source code.
- Respect API rate limits.
- Use pagination when retrieving large datasets.
- Retry only appropriate operations.
- Apply backoff when handling temporary failures.
- Log request metadata without exposing credentials.
- Use sessions for repeated requests.
- Validate downloaded or externally supplied data before processing it.
Be especially careful when retrying POST requests. Repeating a non-idempotent POST operation can create duplicate records or execute the same action multiple times.
Key Takeaways
- Requests is a third-party Python library for working with HTTP services.
- GET is generally used to retrieve resources.
- POST is commonly used to submit or process data.
- Query parameters should be passed through the
paramsargument. - Form data is sent with
data. - JSON request bodies are sent with
json. - Request headers and response headers are separate.
response.textreturns decoded text.response.contentreturns raw bytes.response.json()parses JSON into a Python object.raise_for_status()detects unsuccessful HTTP responses.- Every request should have a timeout.
- Sessions improve repeated communication with the same server.
Conclusion
Python’s Requests library provides an accessible way to communicate with APIs and other HTTP services. A basic request may require only one line of code, but reliable data engineering applications must also handle parameters, headers, response formats, timeouts, unsuccessful status codes, authentication, and security.
Understanding these elements makes it possible to build safer and more dependable API clients, ingestion scripts, and data pipelines.
One-sentence summary: Python’s Requests library enables applications to retrieve and submit web data through HTTP while providing convenient support for query parameters, headers, JSON, error handling, and connection management.
