Making HTTP Requests and Working with APIs
APIs power most of the internet. Every time you check the weather, log into a service, or fetch data from a database, you’re making an HTTP request. Python’s requests library makes this incredibly simple.
This tutorial teaches you how to make HTTP requests, handle responses, and work with REST APIs. By the end, you’ll be able to fetch data from public APIs, send data to servers, and handle errors gracefully.
What is HTTP?
HTTP stands for Hypertext Transfer Protocol. It’s the protocol browsers and servers use to communicate. When you visit a website, your browser sends an HTTP request to a server, which responds with the page content.
HTTP requests have a method that tells the server what you want to do:
- GET retrieves data. Your browser uses this to load pages.
- POST sends data to create something new. Forms use this.
- PUT updates existing data completely.
- DELETE removes data.
Each request goes to a URL, and the server responds with a status code and optional data.
Installing the Requests Library
The requests library is not part of Python’s standard library. You need to install it first:
pip install requests
Once installed, import it like any other package:
import requests
That’s it. You’re ready to make HTTP requests.
Making Your First GET Request
The most common operation is fetching data with GET. You pass a URL to requests.get() and get a response object back:
import requests
response = requests.get('https://api.github.com/users/octocat')
print(response.status_code)
# 200
A status code of 200 means the request succeeded. The response object contains everything the server sent back.
You can also use the response object directly in a conditional. Requests treats status codes below 400 as truthy:
response = requests.get('https://api.github.com/users/octocat')
if response:
print('Success!')
else:
print(f'Failed with code {response.status_code}')
This is useful for quick success checks without manually comparing status codes.
Reading Response Data
The response object gives you access to the data the server sent. For most APIs, you’ll work with JSON:
response = requests.get('https://api.github.com/users/octocat')
data = response.json()
print(data['login'])
# octocat
print(data['public_repos'])
# 72
The .json() method parses the response body as JSON and returns a Python dictionary. If the response isn’t valid JSON, it raises an exception.
Sometimes you need the raw text instead:
response = requests.get('https://example.com')
print(response.text)
# <!doctype html>...
Or raw bytes for binary content like images:
response = requests.get('https://httpbin.org/image')
print(response.content[:20])
# b'\\x89PNG\\r\\n\\x1a\\n...'
Sending Data with POST
GET requests retrieve data. POST requests send data to create something. The json parameter sends JSON-encoded data automatically:
import requests
payload = {'username': 'alice', 'password': 'secret123'}
response = requests.post('https://httpbin.org/post', json=payload)
print(response.status_code)
# 200
data = response.json()
print(data['json'])
# {'username': 'alice', 'password': 'secret123'}
The server receives your data in the request body. Using json=payload sets the Content-Type header to application/json automatically.
For form-encoded data (what HTML forms send), use the data parameter instead:
response = requests.post('https://httpbin.org/post', data={'username': 'alice'})
Handling Errors
Status codes tell you what happened. Codes in the 200s mean success. Codes in the 400s mean client errors (you did something wrong). Codes in the 500s mean server errors (the server failed).
Always check for errors:
response = requests.get('https://api.github.com/users/nonexistent-user')
if response.status_code == 404:
print('User not found')
elif response.status_code >= 500:
print('Server error, try again later')
else:
print('Success')
Requests provides .raise_for_status() to raise an exception for error status codes:
response = requests.get('https://api.github.com/users/nonexistent-user')
response.raise_for_status()
# Raises HTTPError for 4xx and 5xx responses
This is useful when you want your code to stop immediately on errors rather than checking status codes manually.
Customizing Requests with Headers
APIs often require specific headers. Pass a dictionary to the headers parameter:
headers = {'User-Agent': 'MyApp/1.0'}
response = requests.get('https://api.github.com/users/octocat', headers=headers)
print(response.headers['Content-Type'])
# application/json; charset=utf-8
Common headers include:
User-Agent— identifies your applicationAuthorization— passes authentication credentialsAccept— tells the server what response format you want
For authorization, many APIs use a Bearer token:
headers = {'Authorization': 'Bearer YOUR_TOKEN_HERE'}
response = requests.get('https://api.github.com/user', headers=headers)
Never hardcode secrets in your code. Use environment variables instead:
import os
headers = {'Authorization': f'Bearer {os.environ.get("API_TOKEN")}'}
Using Query Parameters
To pass parameters in the URL (like ?search=python), use the params parameter:
params = {'q': 'requests+language:python', 'sort': 'stars'}
response = requests.get('https://api.github.com/search/repositories', params=params)
data = response.json()
print(data['total_count'])
# Number of matching repositories
Requests handles URL encoding automatically. You can pass a dictionary, list of tuples, or bytes.
Common API Patterns
Most REST APIs follow similar patterns. Once you understand these, you can work with almost any API.
Pagination — APIs limit how much data they return. You need to request multiple pages:
all_results = []
page = 1
while True:
response = requests.get(
'https://api.github.com/users/octocat/repos',
params={'page': page, 'per_page': 100}
)
repos = response.json()
if not repos:
break
all_results.extend(repos)
page += 1
Rate limiting — APIs restrict how many requests you can make. Check the headers:
response = requests.get('https://api.github.com/users/octocat')
remaining = response.headers.get('X-RateLimit-Remaining')
print(f'Remaining requests: {remaining}')
Timeouts — Always set a timeout to avoid hanging indefinitely:
response = requests.get('https://slow-api.example.com', timeout=5)
Without a timeout, your code waits forever if the server never responds.
Next Steps
You now have the foundation for working with APIs in Python. The requests library handles the complexity of HTTP so you can focus on the data.
To practice, try fetching data from a public API like:
- JSONPlaceholder (https://jsonplaceholder.typicode.com) — fake data for testing
- OpenWeatherMap — real weather data
- GitHub API — repositories, users, issues
The next tutorial in this series covers working with JSON data in more detail. You’ll learn parsing, serialization, and handling nested data structures.