Python Working with APIs (requests)
Learn to use Python's requests library to make GET and POST calls, send headers and query parameters, handle JSON responses, and manage errors.
The requests library is the standard way to make HTTP calls from Python. It wraps Python's lower-level urllib in a clean API, so fetching a web page or calling a REST API takes one line instead of ten. This chapter covers everything you need: installing requests, making GET and POST calls, sending headers and query parameters, working with JSON responses, uploading files, using sessions, and handling errors robustly.
Installation
requests is not part of the standard library, so you install it with pip:
pip install requestsIf you are working inside a virtual environment (recommended), activate it first so the package is scoped to your project. After installation, verify it works:
import requests
print(requests.__version__) # e.g. 2.32.3Making a GET request
requests.get() sends an HTTP GET request and returns a Response object. This is the most common operation — used for fetching data from APIs, web pages, and files.
import requests
response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
print(response.status_code) # 200
print(response.url) # https://jsonplaceholder.typicode.com/todos/1
print(response.text) # raw response body as a stringExpected output:
200
https://jsonplaceholder.typicode.com/todos/1
{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}Reading the response as JSON
Most modern APIs return JSON. Call .json() on the response instead of parsing .text manually — it calls json.loads() for you and returns a Python dict or list.
import requests
response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
data = response.json()
print(data["title"]) # delectus aut autem
print(data["completed"]) # FalseSee the Python JSON chapter for details on how Python objects map to JSON types.
Sending query parameters
Query parameters are the key-value pairs after the ? in a URL, like ?q=python&page=2. Pass them as a dict to the params argument — requests URL-encodes and appends them automatically.
import requests
params = {
"q": "python requests",
"page": 1,
"per_page": 5,
}
response = requests.get("https://httpbin.org/get", params=params)
# requests builds the full URL for you
print(response.url)
# https://httpbin.org/get?q=python+requests&page=1&per_page=5Always use params= rather than building the URL by hand — it handles special characters and encoding correctly.
Sending request headers
Headers carry metadata: authentication tokens, content-type preferences, API keys, and more. Pass them as a dict to headers=:
import requests
headers = {
"Accept": "application/json",
"Authorization": "Bearer my-api-token",
"User-Agent": "MyApp/1.0",
}
response = requests.get("https://httpbin.org/headers", headers=headers)
print(response.json())Common headers you will send:
| Header | Purpose |
|---|---|
Authorization | Authentication token (Bearer, Basic, etc.) |
Content-Type | Format of the request body (e.g. application/json) |
Accept | Format you want back from the server |
User-Agent | Identifies your client to the server |
X-API-Key | API key in a custom header (varies by service) |
Making a POST request
requests.post() sends data to the server — used to create resources, submit forms, or call actions.
Sending JSON
Pass a Python dict to json=. The library serializes it and sets the Content-Type: application/json header automatically:
import requests
payload = {
"title": "Buy groceries",
"completed": False,
"userId": 1,
}
response = requests.post(
"https://jsonplaceholder.typicode.com/todos",
json=payload,
)
print(response.status_code) # 201 Created
print(response.json())Expected output:
201
{'title': 'Buy groceries', 'completed': False, 'userId': 1, 'id': 201}Sending form data
Some APIs or HTML forms expect application/x-www-form-urlencoded data. Use data= instead of json=:
import requests
form_data = {
"username": "alice",
"password": "secret",
}
response = requests.post("https://httpbin.org/post", data=form_data)
print(response.status_code)Sending files (multipart upload)
To upload a file, open it in binary mode and pass it through files=:
import requests
with open("report.pdf", "rb") as f:
response = requests.post(
"https://httpbin.org/post",
files={"file": f},
)
print(response.status_code)requests encodes the upload as multipart/form-data, which is what most file-upload endpoints expect.
Other HTTP methods
REST APIs use different HTTP verbs for different operations. requests provides one function per method:
import requests
base = "https://jsonplaceholder.typicode.com/todos/1"
# Update a resource (replace entirely)
response = requests.put(base, json={"title": "Updated", "completed": True, "userId": 1})
print(response.status_code) # 200
# Partial update
response = requests.patch(base, json={"completed": True})
print(response.status_code) # 200
# Delete a resource
response = requests.delete(base)
print(response.status_code) # 200Error handling
Checking status codes
The HTTP status code tells you whether the request succeeded. The most important groups:
| Range | Meaning |
|---|---|
| 2xx | Success (200 OK, 201 Created, 204 No Content) |
| 3xx | Redirect (handled automatically by requests) |
| 4xx | Client error (400 Bad Request, 401 Unauthorized, 404 Not Found) |
| 5xx | Server error (500 Internal Server Error, 503 Service Unavailable) |
raise_for_status()
Calling .raise_for_status() on a response raises an HTTPError exception automatically if the status code is 4xx or 5xx. This is the cleanest way to fail fast on bad responses:
import requests
response = requests.get("https://jsonplaceholder.typicode.com/todos/99999")
try:
response.raise_for_status()
data = response.json()
print(data)
except requests.exceptions.HTTPError as err:
print(f"HTTP error: {err}")Without raise_for_status(), a 404 response looks like a success — your code reads an error body and silently processes it.
Handling network errors
Network-level failures (DNS lookup failure, connection refused, timeout) raise requests.exceptions.ConnectionError or requests.exceptions.Timeout. Catch both with the base requests.exceptions.RequestException:
import requests
try:
response = requests.get("https://api.example.com/data", timeout=5)
response.raise_for_status()
data = response.json()
except requests.exceptions.Timeout:
print("The request timed out — server took too long to respond.")
except requests.exceptions.ConnectionError:
print("Could not connect — check your network or the URL.")
except requests.exceptions.HTTPError as err:
print(f"HTTP error {response.status_code}: {err}")
except requests.exceptions.RequestException as err:
print(f"Unexpected error: {err}")This pattern covers the full exception hierarchy: timeout, connection, HTTP error, and the catch-all base class. See Python try/except for a deeper look at exception handling in Python.
Always set a timeout
By default requests will wait forever if the server never responds. Always pass timeout= to avoid hanging programs:
# timeout=(connect_timeout, read_timeout) in seconds
response = requests.get("https://api.example.com/data", timeout=(3, 10))The tuple form sets the connection timeout and the read timeout separately. A read timeout of 10 seconds means "wait up to 10 seconds between bytes after the connection is established."
Using sessions
A requests.Session object persists settings — headers, cookies, authentication — across multiple requests to the same host. It also reuses the underlying TCP connection (connection pooling), which is faster than creating a new connection for every call.
import requests
with requests.Session() as session:
# Set headers once — every request in this session will include them
session.headers.update({
"Authorization": "Bearer my-api-token",
"Accept": "application/json",
})
# All requests reuse the connection and headers
r1 = session.get("https://api.example.com/users")
r2 = session.get("https://api.example.com/posts")
r3 = session.post("https://api.example.com/todos", json={"title": "New"})
print(r1.status_code, r2.status_code, r3.status_code)Use a session whenever you are making more than one request to the same server.
Inspecting the response
The Response object exposes everything the server sent back:
import requests
response = requests.get("https://httpbin.org/get")
print(response.status_code) # 200
print(response.reason) # OK
print(response.headers) # dict of response headers
print(response.headers["Content-Type"]) # application/json
print(response.encoding) # utf-8
print(response.elapsed) # how long the request took
print(response.url) # final URL (after redirects)For binary content (images, PDFs), use response.content (returns bytes) instead of response.text:
import requests
response = requests.get("https://httpbin.org/image/png")
with open("image.png", "wb") as f:
f.write(response.content)Authentication
HTTP Basic Auth
Pass a (username, password) tuple to auth=:
import requests
response = requests.get(
"https://httpbin.org/basic-auth/alice/secret",
auth=("alice", "secret"),
)
print(response.status_code) # 200Bearer token (API keys)
Most modern APIs use a bearer token in the Authorization header:
import requests
headers = {"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs..."}
response = requests.get("https://api.example.com/me", headers=headers)Never hard-code tokens in source files. Load them from environment variables or a secrets manager:
import os
import requests
token = os.environ["API_TOKEN"]
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("https://api.example.com/me", headers=headers)Real-world example — GitHub API
This example demonstrates a complete pattern: session, bearer auth, pagination, error handling, and JSON parsing:
import os
import requests
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
BASE_URL = "https://api.github.com"
with requests.Session() as session:
session.headers.update({
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
})
try:
# Fetch the first page of public repos for a user
response = session.get(
f"{BASE_URL}/users/torvalds/repos",
params={"per_page": 5, "sort": "updated"},
timeout=10,
)
response.raise_for_status()
repos = response.json()
for repo in repos:
print(f"{repo['name']:40s} ★ {repo['stargazers_count']}")
except requests.exceptions.HTTPError as err:
print(f"GitHub API error: {err}")
except requests.exceptions.RequestException as err:
print(f"Network error: {err}")This pattern — session with shared headers, raise_for_status(), scoped try/except — is the production-ready approach for any API client you write.
Concurrent requests with asyncio
For programs that call many endpoints at once, the synchronous requests library blocks on each call. Switch to aiohttp (the async equivalent) and combine it with Python's asyncio module:
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def main():
urls = [
"https://jsonplaceholder.typicode.com/todos/1",
"https://jsonplaceholder.typicode.com/todos/2",
"https://jsonplaceholder.typicode.com/todos/3",
]
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*[fetch(session, u) for u in urls])
for r in results:
print(r["title"])
asyncio.run(main())See the Python asyncio chapter to understand how async/await works before adopting this pattern.
Quick reference
| Task | Code |
|---|---|
| GET request | requests.get(url) |
| GET with params | requests.get(url, params={"key": "val"}) |
| GET with headers | requests.get(url, headers={"Authorization": "Bearer token"}) |
| POST JSON body | requests.post(url, json={"key": "val"}) |
| POST form data | requests.post(url, data={"key": "val"}) |
| Upload a file | requests.post(url, files={"file": open("f.pdf", "rb")}) |
| PUT / PATCH / DELETE | requests.put/patch/delete(url, json=...) |
| Check status | response.status_code |
| Raise on 4xx/5xx | response.raise_for_status() |
| Parse JSON body | response.json() |
| Raw text body | response.text |
| Raw bytes body | response.content |
| Set timeout | requests.get(url, timeout=5) |
| Reuse connection | with requests.Session() as s: ... |
Related chapters
- Python pip — install
requestsand manage project dependencies - Python JSON — understand the JSON encoding that powers most API responses
- Python try/except — write robust exception handling around network calls
- Python Virtual Environments — isolate project dependencies
- Python asyncio — concurrent HTTP calls without blocking