Skip to main content

Module 23 - Working with APIs

APIs (Application Programming Interfaces) allow applications to communicate with each other. Python's requests library makes it easy to work with REST APIs.


1. Introduction to REST APIs​

REST (Representational State Transfer) is an architectural style for web services.

HTTP Methods​

MethodPurposeExample
GETRetrieve dataGet user list
POSTCreate dataCreate new user
PUTUpdate (replace) dataUpdate user info
PATCHPartial updateUpdate user email only
DELETEDelete dataDelete user

Status Codes​

CodeMeaningExample
200OKSuccessful GET
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestInvalid data
401UnauthorizedMissing/invalid auth
404Not FoundResource doesn't exist
500Server ErrorServer-side error

2. The requests Library​

Installation​

pip install requests

2.1 GET Requests​

import requests

# Simple GET request
response = requests.get('https://api.github.com')
print(response.status_code) # 200
print(response.text) # Response body as string
print(response.json()) # Response body as JSON

# GET with query parameters
params = {'q': 'python', 'sort': 'stars'}
response = requests.get('https://api.github.com/search/repositories', params=params)
data = response.json()
print(data['total_count'])

2.2 POST Requests​

import requests

# POST with JSON data
url = 'https://jsonplaceholder.typicode.com/posts'
data = {
'title': 'My Post',
'body': 'This is the content',
'userId': 1
}

response = requests.post(url, json=data)
print(response.status_code) # 201
print(response.json())

# POST with form data
form_data = {'username': 'alice', 'password': 'secret'}
response = requests.post('https://example.com/login', data=form_data)

2.3 PUT and PATCH Requests​

import requests

# PUT - Replace entire resource
url = 'https://jsonplaceholder.typicode.com/posts/1'
data = {
'id': 1,
'title': 'Updated Title',
'body': 'Updated content',
'userId': 1
}
response = requests.put(url, json=data)

# PATCH - Partial update
data = {'title': 'New Title Only'}
response = requests.patch(url, json=data)
print(response.json())

2.4 DELETE Requests​

import requests

url = 'https://jsonplaceholder.typicode.com/posts/1'
response = requests.delete(url)
print(response.status_code) # 200 or 204

3. Headers and Authentication​

3.1 Custom Headers​

import requests

headers = {
'User-Agent': 'MyApp/1.0',
'Accept': 'application/json'
}

response = requests.get('https://api.github.com', headers=headers)
print(response.json())

3.2 Bearer Token Authentication​

import requests

token = 'your_access_token_here'
headers = {'Authorization': f'Bearer {token}'}

response = requests.get('https://api.example.com/protected', headers=headers)

3.3 Basic Authentication​

import requests

# Method 1: Using auth parameter
response = requests.get(
'https://api.example.com/users',
auth=('username', 'password')
)

# Method 2: Using headers
import base64

credentials = base64.b64encode(b'username:password').decode('utf-8')
headers = {'Authorization': f'Basic {credentials}'}
response = requests.get('https://api.example.com/users', headers=headers)

3.4 API Key Authentication​

import requests

# In query parameters
params = {'api_key': 'your_api_key_here'}
response = requests.get('https://api.example.com/data', params=params)

# In headers
headers = {'X-API-Key': 'your_api_key_here'}
response = requests.get('https://api.example.com/data', headers=headers)

4. Error Handling​

4.1 Checking Status Codes​

import requests

response = requests.get('https://api.github.com/invalid-endpoint')

if response.status_code == 200:
print('Success!')
data = response.json()
elif response.status_code == 404:
print('Not found')
elif response.status_code == 500:
print('Server error')
else:
print(f'Error: {response.status_code}')

4.2 Using raise_for_status()​

import requests

try:
response = requests.get('https://api.github.com/invalid')
response.raise_for_status() # Raises exception for 4xx/5xx
data = response.json()
except requests.exceptions.HTTPError as e:
print(f'HTTP error occurred: {e}')
except requests.exceptions.ConnectionError:
print('Connection error')
except requests.exceptions.Timeout:
print('Request timed out')
except requests.exceptions.RequestException as e:
print(f'Error: {e}')

4.3 Comprehensive Error Handling​

import requests
from typing import Optional, Dict

def fetch_data(url: str) -> Optional[Dict]:
"""Fetch data with comprehensive error handling"""
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f'HTTP error: {e}')
print(f'Response: {response.text}')
except requests.exceptions.ConnectionError:
print('Failed to connect to server')
except requests.exceptions.Timeout:
print('Request timed out')
except requests.exceptions.JSONDecodeError:
print('Invalid JSON response')
except Exception as e:
print(f'Unexpected error: {e}')

return None

data = fetch_data('https://api.github.com/users/octocat')
if data:
print(data)

5. Practical Examples​

5.1 GitHub API Client​

import requests
from typing import List, Dict

class GitHubAPI:
BASE_URL = 'https://api.github.com'

def __init__(self, token: str = None):
self.session = requests.Session()
if token:
self.session.headers['Authorization'] = f'token {token}'

def get_user(self, username: str) -> Dict:
"""Get user information"""
response = self.session.get(f'{self.BASE_URL}/users/{username}')
response.raise_for_status()
return response.json()

def get_repos(self, username: str) -> List[Dict]:
"""Get user's repositories"""
response = self.session.get(f'{self.BASE_URL}/users/{username}/repos')
response.raise_for_status()
return response.json()

def search_repositories(self, query: str, sort: str = 'stars') -> List[Dict]:
"""Search repositories"""
params = {'q': query, 'sort': sort}
response = self.session.get(f'{self.BASE_URL}/search/repositories', params=params)
response.raise_for_status()
return response.json()['items']

# Usage
api = GitHubAPI()
user = api.get_user('octocat')
print(f"User: {user['name']}")

repos = api.get_repos('octocat')
print(f"Repositories: {len(repos)}")

5.2 Weather API Client​

import requests
from typing import Dict

class WeatherAPI:
BASE_URL = 'https://api.openweathermap.org/data/2.5'

def __init__(self, api_key: str):
self.api_key = api_key

def get_current_weather(self, city: str) -> Dict:
"""Get current weather for a city"""
params = {
'q': city,
'appid': self.api_key,
'units': 'metric'
}
response = requests.get(f'{self.BASE_URL}/weather', params=params)
response.raise_for_status()
return response.json()

def get_forecast(self, city: str, days: int = 5) -> Dict:
"""Get weather forecast"""
params = {
'q': city,
'appid': self.api_key,
'units': 'metric',
'cnt': days
}
response = requests.get(f'{self.BASE_URL}/forecast', params=params)
response.raise_for_status()
return response.json()

# Usage
# api = WeatherAPI('your_api_key')
# weather = api.get_current_weather('London')
# print(f"Temperature: {weather['main']['temp']}°C")

5.3 REST API CRUD Operations​

import requests
from typing import List, Dict, Optional

class UserAPI:
BASE_URL = 'https://jsonplaceholder.typicode.com'

def get_users(self) -> List[Dict]:
"""Get all users"""
response = requests.get(f'{self.BASE_URL}/users')
response.raise_for_status()
return response.json()

def get_user(self, user_id: int) -> Dict:
"""Get user by ID"""
response = requests.get(f'{self.BASE_URL}/users/{user_id}')
response.raise_for_status()
return response.json()

def create_user(self, user_data: Dict) -> Dict:
"""Create new user"""
response = requests.post(f'{self.BASE_URL}/users', json=user_data)
response.raise_for_status()
return response.json()

def update_user(self, user_id: int, user_data: Dict) -> Dict:
"""Update user"""
response = requests.put(f'{self.BASE_URL}/users/{user_id}', json=user_data)
response.raise_for_status()
return response.json()

def delete_user(self, user_id: int) -> bool:
"""Delete user"""
response = requests.delete(f'{self.BASE_URL}/users/{user_id}')
response.raise_for_status()
return True

# Usage
api = UserAPI()

# Get all users
users = api.get_users()
print(f"Total users: {len(users)}")

# Get specific user
user = api.get_user(1)
print(f"User: {user['name']}")

# Create user
new_user = api.create_user({
'name': 'John Doe',
'email': 'john@example.com'
})
print(f"Created user: {new_user}")

6. Advanced Features​

6.1 Sessions (Connection Pooling)​

import requests

# Reuse connection for multiple requests
session = requests.Session()
session.headers.update({'Authorization': 'Bearer token123'})

# All requests use the same session
response1 = session.get('https://api.example.com/endpoint1')
response2 = session.get('https://api.example.com/endpoint2')
response3 = session.get('https://api.example.com/endpoint3')

session.close()

6.2 Timeouts​

import requests

# Connect timeout: 3s, Read timeout: 10s
try:
response = requests.get('https://api.example.com', timeout=(3, 10))
except requests.exceptions.Timeout:
print('Request timed out')

# Single timeout value (applies to both)
response = requests.get('https://api.example.com', timeout=5)

6.3 Retries​

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

# Configure retry strategy
retry_strategy = Retry(
total=3, # Total retries
backoff_factor=1, # Wait 1, 2, 4 seconds
status_forcelist=[429, 500, 502, 503, 504],
method_whitelist=["HEAD", "GET", "OPTIONS"]
)

adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount("https://", adapter)
session.mount("http://", adapter)

# Requests will automatically retry on failure
response = session.get('https://api.example.com')

6.4 Rate Limiting​

import requests
import time
from functools import wraps

def rate_limit(calls_per_second):
"""Decorator for rate limiting"""
min_interval = 1.0 / calls_per_second
last_called = [0.0]

def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
wait_time = min_interval - elapsed

if wait_time > 0:
time.sleep(wait_time)

result = func(*args, **kwargs)
last_called[0] = time.time()
return result

return wrapper
return decorator

@rate_limit(calls_per_second=2)
def fetch_data(url):
return requests.get(url)

# Max 2 calls per second
for i in range(5):
response = fetch_data(f'https://api.example.com/item/{i}')
print(f'Fetched item {i}')

7. File Uploads and Downloads​

7.1 Upload Files​

import requests

# Upload single file
files = {'file': open('document.pdf', 'rb')}
response = requests.post('https://api.example.com/upload', files=files)

# Upload with additional data
files = {'file': open('image.jpg', 'rb')}
data = {'title': 'My Image', 'description': 'A photo'}
response = requests.post('https://api.example.com/upload', files=files, data=data)

# Multiple files
files = [
('file', open('file1.txt', 'rb')),
('file', open('file2.txt', 'rb'))
]
response = requests.post('https://api.example.com/upload', files=files)

7.2 Download Files​

import requests

# Download file
url = 'https://example.com/file.pdf'
response = requests.get(url)

with open('downloaded_file.pdf', 'wb') as file:
file.write(response.content)

# Stream large files
url = 'https://example.com/large_file.zip'
response = requests.get(url, stream=True)

with open('large_file.zip', 'wb') as file:
for chunk in response.iter_content(chunk_size=8192):
file.write(chunk)

print('Download complete')

8. Pagination​

Handling Paginated Responses​

import requests
from typing import List, Dict

def fetch_all_pages(base_url: str, per_page: int = 100) -> List[Dict]:
"""Fetch all pages from a paginated API"""
all_data = []
page = 1

while True:
params = {'page': page, 'per_page': per_page}
response = requests.get(base_url, params=params)
response.raise_for_status()

data = response.json()
if not data:
break

all_data.extend(data)
page += 1

return all_data

# Usage
# users = fetch_all_pages('https://api.example.com/users')

9. Best Practices​

Create an API Wrapper Class​

import requests
from typing import Dict, Optional

class APIClient:
def __init__(self, base_url: str, api_key: Optional[str] = None):
self.base_url = base_url
self.session = requests.Session()

if api_key:
self.session.headers['Authorization'] = f'Bearer {api_key}'

# Set default timeout
self.session.request = lambda *args, **kwargs: \
requests.Session.request(self.session, *args, timeout=10, **kwargs)

def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict:
"""Make HTTP request with error handling"""
url = f'{self.base_url}/{endpoint.lstrip("/")}'

try:
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f'HTTP error: {e}')
raise
except Exception as e:
print(f'Error: {e}')
raise

def get(self, endpoint: str, **kwargs) -> Dict:
return self._make_request('GET', endpoint, **kwargs)

def post(self, endpoint: str, **kwargs) -> Dict:
return self._make_request('POST', endpoint, **kwargs)

def put(self, endpoint: str, **kwargs) -> Dict:
return self._make_request('PUT', endpoint, **kwargs)

def delete(self, endpoint: str, **kwargs) -> Dict:
return self._make_request('DELETE', endpoint, **kwargs)

# Usage
api = APIClient('https://api.example.com', api_key='your_key')
data = api.get('/users/123')
Best Practices

✅ Use sessions for multiple requests
✅ Always set timeouts
✅ Implement retry logic for unreliable APIs
✅ Handle errors comprehensively
✅ Use environment variables for API keys
✅ Respect rate limits


Summary​

✅ requests library simplifies HTTP communication
✅ Use GET, POST, PUT, PATCH, DELETE for CRUD operations
✅ Handle authentication with headers or auth parameter
✅ Always implement error handling and timeouts
✅ Use sessions for better performance
✅ Respect API rate limits


Next Steps​

In Module 24, you'll learn:

  • Database access with sqlite3
  • SQL basics and queries
  • CRUD operations in databases
  • Connection management

Practice Exercises​

  1. Create a GitHub repository search tool using the GitHub API
  2. Build a weather dashboard that fetches data from multiple cities
  3. Implement a REST API client with automatic retry and rate limiting
  4. Create a file downloader with progress tracking
  5. Build a simple web scraper that respects robots.txt
Challenge

Create a comprehensive API testing framework that:

  • Supports multiple authentication methods
  • Implements automatic retry with exponential backoff
  • Logs all requests and responses
  • Validates response schemas
  • Generates test reports
  • Handles pagination automatically