Skip to main content

Python RESTful APIs

REST (Representational State Transfer) APIs have become the standard method for creating web services. In this guide, you'll learn how to both consume and build RESTful APIs using Python, a language known for its simplicity and powerful web development capabilities.

What is a RESTful API?

A RESTful API is an architectural style for designing networked applications. It uses HTTP requests to access and manipulate data, following specific principles:

  • Resource-based: Everything is a resource identified by a URL
  • Stateless: Each request contains all information needed to process it
  • Standard HTTP methods: Uses GET, POST, PUT, DELETE, etc.
  • Standard data formats: Typically uses JSON or XML

Consuming RESTful APIs with Python

Before building your own API, let's learn how to consume existing APIs with Python's popular requests library.

Setting Up

First, install the requests library:

bash
pip install requests

Making Basic Requests

Let's start with a simple GET request to fetch data from a public API:

python
import requests

# Make a GET request to a public API
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')

# Check if the request was successful
if response.status_code == 200:
# Parse the JSON response
data = response.json()
print(f"Post title: {data['title']}")
print(f"Post body: {data['body']}")
else:
print(f"Request failed with status code: {response.status_code}")

Output:

Post title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Post body: quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam nostrum rerum est autem sunt rem eveniet architecto

Working with Different HTTP Methods

REST APIs use different HTTP methods for different operations:

python
import requests
import json

# Base URL for our API
base_url = 'https://jsonplaceholder.typicode.com'

# GET: Retrieve data
get_response = requests.get(f'{base_url}/posts/1')
print(f"GET status: {get_response.status_code}")

# POST: Create new data
new_post = {
'title': 'New Post',
'body': 'This is the content of my new post',
'userId': 1
}
post_response = requests.post(
f'{base_url}/posts',
data=json.dumps(new_post),
headers={'Content-type': 'application/json; charset=UTF-8'}
)
print(f"POST status: {post_response.status_code}")
print(f"Created post ID: {post_response.json().get('id')}")

# PUT: Update existing data
updated_post = {
'id': 1,
'title': 'Updated Title',
'body': 'Updated content',
'userId': 1
}
put_response = requests.put(
f'{base_url}/posts/1',
data=json.dumps(updated_post),
headers={'Content-type': 'application/json; charset=UTF-8'}
)
print(f"PUT status: {put_response.status_code}")

# DELETE: Remove data
delete_response = requests.delete(f'{base_url}/posts/1')
print(f"DELETE status: {delete_response.status_code}")

Output:

GET status: 200
POST status: 201
Created post ID: 101
PUT status: 200
DELETE status: 200

Handling Authentication

Many APIs require authentication. Here's how to use different authentication methods:

python
import requests

# Basic authentication
response = requests.get(
'https://api.example.com/data',
auth=('username', 'password')
)

# API key authentication (in header)
headers = {
'Authorization': 'Bearer YOUR_API_KEY'
}
response = requests.get(
'https://api.example.com/data',
headers=headers
)

# API key as a parameter
params = {
'api_key': 'YOUR_API_KEY'
}
response = requests.get(
'https://api.example.com/data',
params=params
)

Building RESTful APIs with Flask

Now let's learn how to build our own RESTful API using Flask, a lightweight Python web framework.

Setting Up Flask

First, install Flask and its extension for RESTful APIs:

bash
pip install flask flask-restful

Creating a Simple REST API

Let's create a simple API for managing a collection of books:

python
from flask import Flask, request, jsonify

app = Flask(__name__)

# In-memory data store
books = [
{"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
{"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee"}
]

# GET all books
@app.route('/api/books', methods=['GET'])
def get_books():
return jsonify(books)

# GET a specific book
@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book:
return jsonify(book)
return jsonify({"error": "Book not found"}), 404

# POST a new book
@app.route('/api/books', methods=['POST'])
def add_book():
if not request.json or not 'title' in request.json:
return jsonify({"error": "Title is required"}), 400

# Generate a new ID
new_id = max(book['id'] for book in books) + 1 if books else 1

book = {
'id': new_id,
'title': request.json['title'],
'author': request.json.get('author', '')
}

books.append(book)
return jsonify(book), 201

# PUT (update) a book
@app.route('/api/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if not book:
return jsonify({"error": "Book not found"}), 404

if not request.json:
return jsonify({"error": "No data provided"}), 400

book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])

return jsonify(book)

# DELETE a book
@app.route('/api/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
global books
initial_length = len(books)
books = [b for b in books if b['id'] != book_id]

if len(books) < initial_length:
return jsonify({"message": "Book deleted"}), 200
return jsonify({"error": "Book not found"}), 404

if __name__ == '__main__':
app.run(debug=True)

To run this API, save the code to a file like app.py and execute it:

bash
python app.py

Your API will be available at http://127.0.0.1:5000/.

Testing the API

You can use tools like Postman, or Python's requests library to test your API:

python
import requests

base_url = 'http://127.0.0.1:5000/api/books'

# Get all books
response = requests.get(base_url)
print("All books:", response.json())

# Add a new book
new_book = {"title": "1984", "author": "George Orwell"}
response = requests.post(base_url, json=new_book)
print("Added book:", response.json())

# Get a specific book
book_id = response.json()['id'] # Use the ID from the book we just added
response = requests.get(f"{base_url}/{book_id}")
print(f"Book {book_id}:", response.json())

# Update a book
updated_info = {"title": "1984", "author": "George Orwell (Updated)"}
response = requests.put(f"{base_url}/{book_id}", json=updated_info)
print("Updated book:", response.json())

# Delete a book
response = requests.delete(f"{base_url}/{book_id}")
print("Delete response:", response.json())

Using Flask-RESTful for More Structured APIs

Flask-RESTful is an extension that adds support for quickly building REST APIs. It's a common choice for more complex APIs:

python
from flask import Flask
from flask_restful import Resource, Api, reqparse

app = Flask(__name__)
api = Api(app)

# In-memory data store
books = [
{"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
{"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee"}
]

# Create parser for book data
parser = reqparse.RequestParser()
parser.add_argument('title', type=str, required=True, help='Title cannot be blank')
parser.add_argument('author', type=str, required=False)

# Resource for collection of books
class BookList(Resource):
def get(self):
return books

def post(self):
args = parser.parse_args()
new_id = max(book['id'] for book in books) + 1 if books else 1

book = {
'id': new_id,
'title': args['title'],
'author': args['author'] or ''
}

books.append(book)
return book, 201

# Resource for a single book
class Book(Resource):
def get(self, book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book:
return book
return {"error": "Book not found"}, 404

def put(self, book_id):
book = next((b for b in books if b['id'] == book_id), None)
if not book:
return {"error": "Book not found"}, 404

args = parser.parse_args()
book['title'] = args['title']
book['author'] = args['author'] or book['author']
return book

def delete(self, book_id):
global books
initial_length = len(books)
books = [b for b in books if b['id'] != book_id]

if len(books) < initial_length:
return {"message": "Book deleted"}, 200
return {"error": "Book not found"}, 404

# Register resources
api.add_resource(BookList, '/api/books')
api.add_resource(Book, '/api/books/<int:book_id>')

if __name__ == '__main__':
app.run(debug=True)

Best Practices for RESTful APIs

When designing your APIs, keep these best practices in mind:

  1. Use proper HTTP methods:

    • GET for retrieving data
    • POST for creating data
    • PUT/PATCH for updating data
    • DELETE for removing data
  2. Return appropriate status codes:

    • 200 OK: Request succeeded
    • 201 Created: Resource created successfully
    • 400 Bad Request: Invalid request format
    • 404 Not Found: Resource not found
    • 500 Server Error: Something went wrong on the server
  3. Version your API: Add a version number in the URL path (e.g., /api/v1/books)

  4. Implement proper error handling: Return meaningful error messages and appropriate status codes

  5. Use pagination for large datasets:

python
@app.route('/api/books', methods=['GET'])
def get_books():
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 10))

start = (page - 1) * per_page
end = start + per_page

book_slice = books[start:end]

return jsonify({
'books': book_slice,
'total': len(books),
'page': page,
'per_page': per_page,
'pages': (len(books) + per_page - 1) // per_page
})
  1. Document your API: Use tools like Swagger/OpenAPI to document your endpoints

Real-World Example: Building a Weather API Client

Let's create a practical example by building a client for the OpenWeatherMap API:

python
import requests
import os
from datetime import datetime

class WeatherClient:
def __init__(self, api_key=None):
self.api_key = api_key or os.environ.get('OPENWEATHER_API_KEY')
if not self.api_key:
raise ValueError("API key is required")

self.base_url = "https://api.openweathermap.org/data/2.5"

def get_current_weather(self, city, units="metric"):
"""Get current weather for a city"""
endpoint = f"{self.base_url}/weather"
params = {
"q": city,
"appid": self.api_key,
"units": units
}

response = requests.get(endpoint, params=params)

if response.status_code == 200:
data = response.json()
return {
"city": data["name"],
"country": data["sys"]["country"],
"temperature": data["main"]["temp"],
"feels_like": data["main"]["feels_like"],
"description": data["weather"][0]["description"],
"humidity": data["main"]["humidity"],
"wind_speed": data["wind"]["speed"],
"timestamp": datetime.fromtimestamp(data["dt"]).strftime('%Y-%m-%d %H:%M:%S')
}
elif response.status_code == 404:
return {"error": "City not found"}
else:
return {"error": f"API request failed with status code {response.status_code}"}

def get_forecast(self, city, days=5, units="metric"):
"""Get weather forecast for a city"""
endpoint = f"{self.base_url}/forecast"
params = {
"q": city,
"appid": self.api_key,
"units": units,
"cnt": days * 8 # 8 measurements per day (every 3 hours)
}

response = requests.get(endpoint, params=params)

if response.status_code == 200:
data = response.json()
forecasts = []

for item in data["list"]:
forecasts.append({
"datetime": item["dt_txt"],
"temperature": item["main"]["temp"],
"description": item["weather"][0]["description"],
"humidity": item["main"]["humidity"],
"wind_speed": item["wind"]["speed"]
})

return {
"city": data["city"]["name"],
"country": data["city"]["country"],
"forecasts": forecasts
}
elif response.status_code == 404:
return {"error": "City not found"}
else:
return {"error": f"API request failed with status code {response.status_code}"}

# Example usage
if __name__ == "__main__":
# Get API key from OpenWeatherMap website
api_key = "your_api_key_here"
weather = WeatherClient(api_key)

# Get current weather
current = weather.get_current_weather("London")
print(f"Current weather in {current['city']}:")
print(f"Temperature: {current['temperature']}°C")
print(f"Description: {current['description']}")
print(f"Humidity: {current['humidity']}%")

# Get forecast
forecast = weather.get_forecast("London", days=2)
print("\nForecast:")
for f in forecast["forecasts"][:3]: # Show first 3 forecast periods
print(f"{f['datetime']}: {f['temperature']}°C, {f['description']}")

To use this client, you'll need to sign up for a free API key at OpenWeatherMap.

Summary

In this guide, you learned how to:

  • Understand the principles of RESTful APIs
  • Consume REST APIs using Python's requests library
  • Build your own REST API using Flask and Flask-RESTful
  • Follow best practices for API design
  • Create a real-world example with the OpenWeatherMap API

RESTful APIs are a fundamental component of modern web development, and Python provides excellent tools for both consuming and building them. As you continue your journey, you'll find these skills invaluable for creating scalable and maintainable web applications.

Additional Resources

  1. Flask Documentation
  2. Flask-RESTful Documentation
  3. Requests Library Documentation
  4. RESTful API Design Best Practices
  5. Postman - API Testing Tool

Exercises

  1. Extend the book API to include search functionality (search by title or author)
  2. Add user authentication to the Flask API using JWT tokens
  3. Create a client for a public API of your choice (GitHub, Twitter, Spotify, etc.)
  4. Modify the Weather API client to include error handling and automatic retries
  5. Implement pagination in the book API for large collections


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)