Working with APIs - Requests Library
APIs are like restaurant waiters. You (your program) tell the waiter (API) what you want (request), and they bring back food (data) from the kitchen (server). The requests library is your way of communicating with these waiters over the internet.
An **API** (Application Programming Interface) allows different software applications to communicate with each other. In web development, **REST APIs** are the most common. They use standard HTTP methods to perform operations on resources (data). The `requests` library in Python makes it simple to send HTTP requests and handle responses. APIs typically return data in **JSON** format, which you learned to parse in Lesson 20.
## HTTP Methods (CRUD Operations)
REST APIs use HTTP methods to indicate the desired action:
- **GET** – Retrieve data (read).
- **POST** – Create new data.
- **PUT** – Update existing data (replace entirely).
- **PATCH** – Partially update existing data.
- **DELETE** – Remove data.
## Making a Basic GET Request
```python
import requests
response = requests.get('https://api.example.com/users')
```
- `response.status_code` – HTTP status code (200 = OK, 404 = Not Found, etc.).
- `response.text` – raw response as a string.
- `response.json()` – parses JSON response into Python dict/list.
- `response.headers` – response headers (e.g., content type, rate limits).
## Query Parameters
Add parameters to URLs using the `params` argument. The library automatically encodes them.
```python
params = {'q': 'python', 'page': 2}
response = requests.get('https://api.example.com/search', params=params)
```
## Headers and Authentication
Headers provide metadata (e.g., `User-Agent`, `Content-Type`). Authentication often uses an **API key** or **Bearer token**.
```python
headers = {'Authorization': 'Bearer YOUR_TOKEN'}
response = requests.get(url, headers=headers)
```
For Basic Auth, use the `auth` parameter:
```python
from requests.auth import HTTPBasicAuth
response = requests.get(url, auth=HTTPBasicAuth('user', 'pass'))
```
## Sending Data (POST, PUT, PATCH)
Send JSON data using the `json` parameter (automatically sets `Content-Type: application/json`).
```python
new_user = {'name': 'Alice', 'email': 'alice@example.com'}
response = requests.post('https://api.example.com/users', json=new_user)
```
## Error Handling and Status Codes
Always check for errors. Use `response.raise_for_status()` to raise an exception for 4xx/5xx responses. Catch specific exceptions.
```python
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP error: {e}")
except requests.exceptions.ConnectionError:
print("Connection failed")
except requests.exceptions.Timeout:
print("Request timed out")
```
## Rate Limiting and Retries
APIs often limit how many requests you can make per minute. If you receive a `429 Too Many Requests` or `503 Service Unavailable`, implement **exponential backoff** – wait and retry with increasing delays.
```python
for attempt in range(max_retries):
try:
response = requests.get(url)
if response.status_code == 200:
return response.json()
elif response.status_code in (429, 500, 502, 503, 504):
wait = 2 ** attempt # exponential backoff
time.sleep(wait)
else:
response.raise_for_status()
except requests.exceptions.RequestException:
continue
```
## Simulating API Calls for Learning
When you don't have an actual API key or want to avoid network calls, you can **mock** (simulate) responses. The examples in this lesson use simulated data to demonstrate concepts without requiring internet access or real API keys.
## Real‑World Applications
- Fetching weather data (OpenWeatherMap).
- Interacting with GitHub (repositories, issues).
- Sending messages via Telegram or Slack bots.
- Integrating with payment gateways (Stripe, PayPal).
- Getting news headlines (NewsAPI).
## Best Practices
- Always set a `User-Agent` header to identify your application.
- Use timeouts to avoid hanging requests: `requests.get(url, timeout=5)`.
- Store API keys in environment variables – never hard‑code them.
- Respect `Retry-After` headers when rate limited.
- Cache responses when possible to reduce API calls.
- Use sessions for multiple requests to the same host: `session = requests.Session()` (reuses TCP connections).
## Common Mistakes
- Forgetting to call `.json()` – accessing `response['key']` directly (gives TypeError).
- Hard‑coding sensitive keys – use environment variables or config files.
- Not handling non‑200 responses – the program crashes.
- Ignoring rate limits – your IP may get banned.
- Mixing up GET and POST – sending data with GET (URL length limits, security).
## Practice Exercises
1. Use the `requests` library to fetch posts from `https://jsonplaceholder.typicode.com/posts` and print the first 3 titles.
2. Send a POST request to `https://jsonplaceholder.typicode.com/posts` with a new post (title, body, userId) and print the response.
3. Build a simple CLI weather app that asks for a city and displays current temperature using OpenWeatherMap API (sign up for free key).
4. Implement retry logic for a failing API call (simulated).
5. Use the GitHub API to list a user's public repositories (no authentication needed for public data).
This lesson provides **9 complete examples** covering GET requests, HTTP methods, query parameters, headers and authentication, error handling, a simulated weather API, a GitHub API client, rate limiting with retries, and a news API client.
# Working with APIs - Requests Library
import requests
import json
import time
print("WORKING WITH APIS - REQUESTS LIBRARY")
print("=" * 60)
# Note: Install requests library first: pip install requests
# Example 1: Basic GET request
print("\n1. BASIC GET REQUEST")
print("-" * 30)
# Using a free public API for testing
print("Making request to JSONPlaceholder API...")
# In real code:
# response = requests.get('https://jsonplaceholder.typicode.com/posts/1')
# For demonstration without internet, simulate response
class MockResponse:
def __init__(self, status_code, text):
self.status_code = status_code
self.text = text
self.headers = {'Content-Type': 'application/json'}
def json(self):
return json.loads(self.text)
def raise_for_status(self):
if self.status_code != 200:
raise Exception(f"HTTP Error: {self.status_code}")
# Simulate successful API response
mock_data = '''
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
'''
response = MockResponse(200, mock_data)
print(f"Status Code: {response.status_code}")
print(f"Content Type: {response.headers.get('Content-Type')}")
print(f"\nResponse JSON:")
print(json.dumps(response.json(), indent=2))
# Extract data
post_data = response.json()
print(f"\nExtracted Data:")
print(f"Title: {post_data['title']}")
print(f"User ID: {post_data['userId']}")
print(f"Body preview: {post_data['body'][:50]}...")
# Example 2: Different HTTP methods
print("\n\n2. HTTP METHODS")
print("-" * 30)
print("Common HTTP methods for REST APIs:")
print("GET - Retrieve data")
print("POST - Create new data")
print("PUT - Update existing data")
print("DELETE - Remove data")
print("PATCH - Partial update")
# Simulate different requests
def demonstrate_http_methods():
base_url = "https://api.example.com/users"
print("\nSimulating API calls:")
# GET - Retrieve users
print("1. GET /users - Retrieve all users")
# response = requests.get(base_url)
print(" Returns: List of users")
# GET with ID - Retrieve specific user
print("\n2. GET /users/123 - Retrieve user with ID 123")
# response = requests.get(f"{base_url}/123")
print(" Returns: User details")
# POST - Create new user
print("\n3. POST /users - Create new user")
new_user = {
"name": "John Doe",
"email": "john@example.com",
"age": 30
}
# response = requests.post(base_url, json=new_user)
print(f" Sending: {json.dumps(new_user, indent=2)}")
print(" Returns: Created user with ID")
# PUT - Update user
print("\n4. PUT /users/123 - Update user")
updated_user = {"name": "John Updated", "age": 31}
# response = requests.put(f"{base_url}/123", json=updated_user)
print(" Returns: Updated user")
# DELETE - Remove user
print("\n5. DELETE /users/123 - Delete user")
# response = requests.delete(f"{base_url}/123")
print(" Returns: Success status")
demonstrate_http_methods()
# Example 3: Working with query parameters
print("\n\n3. QUERY PARAMETERS")
print("-" * 30)
print("Adding query parameters to API requests:")
# Simulate search with parameters
search_params = {
"q": "python programming",
"page": 1,
"limit": 10,
"sort": "relevance"
}
print(f"\nSearch Parameters: {search_params}")
print("\nEquivalent URL: https://api.example.com/search?q=python+programming&page=1&limit=10&sort=relevance")
# In real code:
# response = requests.get(
# "https://api.example.com/search",
# params=search_params
# )
# Example 4: Headers and Authentication
print("\n\n4. HEADERS AND AUTHENTICATION")
print("-" * 30)
print("Common headers in API requests:")
headers = {
"User-Agent": "MyPythonApp/1.0",
"Content-Type": "application/json",
"Accept": "application/json",
}
print(f"\nHeaders: {json.dumps(headers, indent=2)}")
# Authentication examples
print("\nAuthentication methods:")
print("1. API Key:")
print(" headers = {'X-API-Key': 'your_api_key_here'}")
print(" response = requests.get(url, headers=headers)")
print("\n2. Bearer Token (JWT):")
print(" headers = {'Authorization': 'Bearer your_token_here'}")
print("\n3. Basic Auth:")
print(" from requests.auth import HTTPBasicAuth")
print(" response = requests.get(url, auth=HTTPBasicAuth('username', 'password'))")
# Example 5: Error handling with APIs
print("\n\n5. ERROR HANDLING")
print("-" * 30)
def make_api_request(url):
"""Make API request with comprehensive error handling"""
try:
# Simulate different responses
scenarios = [
(200, 'Success'),
(400, 'Bad Request'),
(401, 'Unauthorized'),
(404, 'Not Found'),
(429, 'Too Many Requests'),
(500, 'Internal Server Error')
]
import random
status_code, message = random.choice(scenarios)
print(f"Request to: {url}")
print(f"Status Code: {status_code} ({message})")
if status_code == 200:
return {"status": "success", "data": {"message": "API call successful"}}
elif status_code == 400:
return {"status": "error", "message": "Bad request - check your parameters"}
elif status_code == 401:
return {"status": "error", "message": "Unauthorized - check your API key"}
elif status_code == 404:
return {"status": "error", "message": "Resource not found"}
elif status_code == 429:
return {"status": "error", "message": "Rate limit exceeded - wait and retry"}
elif status_code == 500:
return {"status": "error", "message": "Server error - try again later"}
except requests.exceptions.Timeout:
return {"status": "error", "message": "Request timed out"}
except requests.exceptions.ConnectionError:
return {"status": "error", "message": "Connection error"}
except Exception as e:
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
# Test error handling
print("Testing API error handling:")
for i in range(3):
result = make_api_request(f"https://api.example.com/data/{i}")
print(f"Result: {result['status']} - {result.get('message', 'Success')}")
print()
# Example 6: Working with real public API (simulated)
print("\n\n6. WORKING WITH REAL PUBLIC API (SIMULATED)")
print("-" * 30)
# Note: These are simulated responses to avoid actual API calls
class WeatherAPI:
"""Simulated weather API client"""
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.weather.example.com"
def get_current_weather(self, city):
"""Get current weather for a city"""
# Simulate API response
weather_data = {
"location": {
"name": city,
"country": "US",
"localtime": "2024-03-15 14:30"
},
"current": {
"temp_c": 22.5,
"temp_f": 72.5,
"condition": {
"text": "Sunny",
"icon": "//cdn.weatherapi.com/weather/64x64/day/113.png"
},
"wind_kph": 15.2,
"humidity": 65,
"feelslike_c": 23.1
}
}
return {
"status": "success",
"data": weather_data
}
def get_forecast(self, city, days=3):
"""Get weather forecast"""
# Simulate forecast data
forecast = {
"location": {"name": city},
"forecast": {"forecastday": []}
}
for i in range(days):
date = f"2024-03-{15 + i}"
forecast["forecast"]["forecastday"].append({
"date": date,
"day": {
"maxtemp_c": 20 + i,
"mintemp_c": 10 + i,
"condition": {"text": ["Sunny", "Partly Cloudy", "Rainy"][i % 3]}
}
})
return {
"status": "success",
"data": forecast
}
# Use the weather API
print("Using Weather API:")
weather_client = WeatherAPI("your_api_key_here")
# Get current weather
result = weather_client.get_current_weather("New York")
if result["status"] == "success":
data = result["data"]
print(f"\nCurrent Weather in {data['location']['name']}:")
print(f"Temperature: {data['current']['temp_c']}°C ({data['current']['temp_f']}°F)")
print(f"Condition: {data['current']['condition']['text']}")
print(f"Feels like: {data['current']['feelslike_c']}°C")
print(f"Humidity: {data['current']['humidity']}%")
print(f"Wind: {data['current']['wind_kph']} km/h")
# Get forecast
print("\n3-Day Forecast:")
forecast_result = weather_client.get_forecast("New York", 3)
if forecast_result["status"] == "success":
forecast = forecast_result["data"]["forecast"]["forecastday"]
for day in forecast:
print(f"{day['date']}: {day['day']['maxtemp_c']}°C / {day['day']['mintemp_c']}°C - {day['day']['condition']['text']}")
# Example 7: Building a complete API client
print("\n\n7. BUILDING A COMPLETE API CLIENT")
print("-" * 30)
class GitHubAPI:
"""GitHub API client for demonstration"""
def __init__(self, username=None, token=None):
self.base_url = "https://api.github.com"
self.username = username
self.token = token
self.headers = {
"Accept": "application/vnd.github.v3+json"
}
if token:
self.headers["Authorization"] = f"token {token}"
def get_user_info(self, username):
"""Get GitHub user information"""
# Simulate API response
user_data = {
"login": username,
"name": "John Doe",
"public_repos": 24,
"followers": 128,
"following": 56,
"created_at": "2018-03-15T10:30:00Z"
}
return user_data
def get_user_repos(self, username, sort="updated", per_page=10):
"""Get user's repositories"""
# Simulate repositories
repos = []
repo_names = ["web-scraper", "todo-app", "weather-app", "blog", "ecommerce"]
for i, name in enumerate(repo_names):
repos.append({
"name": name,
"description": f"A {name.replace('-', ' ')} project",
"stargazers_count": i * 5 + 3,
"forks_count": i * 2 + 1,
"updated_at": f"2024-03-{10 + i}T14:30:00Z",
"html_url": f"https://github.com/{username}/{name}"
})
return repos
def create_repo(self, name, description="", private=False):
"""Create a new repository"""
if not self.token:
return {"error": "Authentication required"}
# Simulate creation
new_repo = {
"name": name,
"full_name": f"{self.username}/{name}",
"description": description,
"private": private,
"html_url": f"https://github.com/{self.username}/{name}",
"created_at": "2024-03-15T14:30:00Z"
}
return new_repo
# Use GitHub API
print("Using GitHub API Client:")
github = GitHubAPI(username="johncoder", token="fake_token")
# Get user info
print("\nUser Information:")
user_info = github.get_user_info("johncoder")
print(f"Username: {user_info['login']}")
print(f"Name: {user_info.get('name', 'Not provided')}")
print(f"Public Repos: {user_info['public_repos']}")
print(f"Followers: {user_info['followers']}")
print(f"Account created: {user_info['created_at'][:10]}")
# Get repositories
print("\nTop Repositories:")
repos = github.get_user_repos("johncoder", per_page=3)
for repo in repos:
print(f"\n{repo['name']}:")
print(f" {repo['description']}")
print(f" Stars: {repo['stargazers_count']}, Forks: {repo['forks_count']}")
print(f" Updated: {repo['updated_at'][:10]}")
print(f" URL: {repo['html_url']}")
# Example 8: Rate limiting and retries
print("\n\n8. RATE LIMITING AND RETRIES")
print("-" * 30)
print("Handling API rate limits:")
class APIClientWithRetry:
"""API client with retry logic"""
def __init__(self, max_retries=3):
self.max_retries = max_retries
def make_request_with_retry(self, url):
"""Make request with exponential backoff retry"""
for attempt in range(self.max_retries):
print(f"Attempt {attempt + 1}/{self.max_retries}")
# Simulate request
# In real code: response = requests.get(url)
# Simulate different outcomes
import random
outcomes = ["success", "rate_limit", "server_error", "timeout"]
outcome = random.choice(outcomes)
if outcome == "success":
print(" ✓ Request successful")
return {"status": "success", "data": "API response data"}
elif outcome == "rate_limit":
print(f" ⚠ Rate limited. Waiting...")
wait_time = (2 ** attempt) # Exponential backoff: 1, 2, 4 seconds
# time.sleep(wait_time)
elif outcome == "server_error":
print(f" ⚠ Server error (500). Waiting...")
wait_time = (2 ** attempt)
# time.sleep(wait_time)
elif outcome == "timeout":
print(f" ⚠ Request timed out")
if attempt < self.max_retries - 1:
print(f" Retrying...")
print(" ✗ All retries failed")
return {"status": "error", "message": "Failed after retries"}
# Test retry logic
print("\nTesting retry logic:")
client = APIClientWithRetry(max_retries=3)
result = client.make_request_with_retry("https://api.example.com/data")
print(f"\nFinal result: {result['status']}")
# Example 9: Real-world project - News API client
print("\n\n9. REAL-WORLD PROJECT: NEWS API CLIENT")
print("-" * 30)
class NewsAPIClient:
"""Client for news API (simulated)"""
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://newsapi.org/v2"
def get_top_headlines(self, country="us", category=None, page_size=10):
"""Get top headlines"""
# Simulate news data
headlines = []
categories = ["technology", "business", "entertainment", "health", "science"]
for i in range(page_size):
cat = category if category else categories[i % len(categories)]
headlines.append({
"title": f"Breaking News in {cat.title()} #{i+1}",
"description": f"This is a sample news article about {cat}.",
"url": f"https://example.com/news/{i+1}",
"publishedAt": f"2024-03-{15 - i}T10:30:00Z",
"source": {"name": f"News Source {i+1}"},
"category": cat
})
return {
"status": "ok",
"totalResults": len(headlines),
"articles": headlines
}
def search_news(self, query, from_date=None, to_date=None, sort_by="relevancy"):
"""Search news articles"""
# Simulate search results
results = []
for i in range(5):
results.append({
"title": f"{query.title()} News {i+1}",
"description": f"This article discusses {query} in detail.",
"url": f"https://example.com/{query.replace(' ', '-')}-{i+1}",
"publishedAt": "2024-03-15T10:30:00Z",
"source": {"name": "Tech News"}
})
return {
"status": "ok",
"totalResults": len(results),
"articles": results
}
# Use news API
print("News API Client Demo:")
news_client = NewsAPIClient("your_news_api_key")
# Get top headlines
print("\nTop Headlines (Technology):")
top_news = news_client.get_top_headlines(country="us", category="technology", page_size=3)
for i, article in enumerate(top_news["articles"], 1):
print(f"\n{i}. {article['title']}")
print(f" Source: {article['source']['name']}")
print(f" Published: {article['publishedAt'][:10]}")
print(f" {article['description'][:60]}...")
# Search news
print("\n\nSearch Results for 'Python':")
search_results = news_client.search_news("python")
for i, article in enumerate(search_results["articles"][:2], 1):
print(f"\n{i}. {article['title']}")
print(f" {article['description']}")
print(f" Read more: {article['url']}")
→ Run this code interactively