Working with APIs

Chapter 9: Working with APIs

APIs (Application Programming Interfaces) connect programs to external services and data sources. Instead of building everything from scratch, you leverage existing APIs to access weather data, social media, databases, payment systems, and more. Python excels at working with web APIs, making it simple to fetch, process, and utilize external data in your applications.

For example, rather than maintaining a weather database, you call a weather API to get current conditions. Instead of building a payment processor, you integrate Stripe's API. APIs transform isolated programs into connected applications that interact with the broader internet ecosystem.

Understanding APIs and REST

Most modern web APIs follow REST (Representational State Transfer) principles. REST APIs use standard HTTP methods to interact with resources:

  • GET - Retrieve data (read)
  • POST - Create new data
  • PUT/PATCH - Update existing data
  • DELETE - Remove data

Each API provides endpoints—URLs that represent specific resources or actions. For instance:

  • https://api.weather.com/current - Get current weather
  • https://api.github.com/users/alice - Get user information

REST APIs return data in standard formats, most commonly JSON (JavaScript Object Notation), which Python handles natively with the json module.

JSON: The Language of APIs

JSON is a lightweight text format for data exchange. It looks like Python dictionaries and lists, making it natural to work with:

The json.loads() function parses JSON strings into Python objects (dictionaries, lists). The json.dumps() function converts Python objects to JSON strings. This bidirectional conversion lets you receive API data and send formatted requests.

HTTP Requests in Python

Traditional Python environments use the requests library for HTTP calls. However, browser/WASM environments like Pyodide handle HTTP differently, using the browser's fetch API through Python's js module.

Traditional Python Approach (Desktop/Server)

Browser/WASM Approach with Pyodide

In browser environments like Pyodide, use the js module to access browser APIs:

# Demonstrating JSON data parsing
import json

# Simulated API response (in real code, this would come from fetch/requests)
api_response = '''
{
    "user": {
        "id": 123,
        "name": "Alice",
        "email": "alice@example.com"
    },
    "posts": [
        {"id": 1, "title": "First Post"},
        {"id": 2, "title": "Second Post"}
    ]
}
'''

# Parse JSON string to Python dict
data = json.loads(api_response)

# Access data
print(f"User: {data['user']['name']}")
print(f"Email: {data['user']['email']}")
print(f"Number of posts: {len(data['posts'])}")
for post in data['posts']:
    print(f"  - {post['title']}")

The browser's fetch API is asynchronous, requiring async/await syntax. This approach works in web environments where Pyodide runs.

Parsing API Responses

Once you receive JSON data, parse and extract what you need:

# Example API response (as JSON string)
response_json = '''
{
  "temperature": 72,
  "conditions": "sunny",
  "humidity": 45,
  "forecast": [
    {"day": "Monday", "high": 75, "low": 60},
    {"day": "Tuesday", "high": 73, "low": 58}
  ]
}
'''

weather = json.loads(response_json)

# Access data like a dictionary
print(f"Current temperature: {weather['temperature']}°F")
print(f"Conditions: {weather['conditions']}")

# Iterate through nested data
for day in weather['forecast']:
    print(f"{day['day']}: High {day['high']}°F, Low {day['low']}°F")

API responses often contain nested structures. Navigate them using dictionary keys and list indices, just like regular Python data structures.

HTTP Status Codes

APIs communicate success or failure through HTTP status codes:

  • 200 OK - Request succeeded
  • 201 Created - Resource created successfully
  • 400 Bad Request - Invalid request format
  • 401 Unauthorized - Authentication required
  • 404 Not Found - Resource doesn't exist
  • 500 Internal Server Error - Server-side error

Always check status codes before processing responses:

# Simulating status checking (concept example)
def handle_response(status, data):
    if status == 200:
        return json.loads(data)
    elif status == 404:
        print("Resource not found")
        return None
    elif status == 401:
        print("Authentication required")
        return None
    else:
        print(f"Unexpected status: {status}")
        return None

# Example usage
result = handle_response(200, '{"message": "Success"}')
print(result)  # Output: {'message': 'Success'}

Proper status handling makes your code robust, gracefully managing API errors rather than crashing.

Working with API Data

Real-world API integration involves fetching, parsing, transforming, and using data:

# Processing user data from an API response
users_json = '''
[
  {"id": 1, "name": "Alice", "email": "alice@example.com", "active": true},
  {"id": 2, "name": "Bob", "email": "bob@example.com", "active": false},
  {"id": 3, "name": "Charlie", "email": "charlie@example.com", "active": true}
]
'''

users = json.loads(users_json)

# Filter active users
active_users = [user for user in users if user["active"]]
print(f"Active users: {len(active_users)}")

# Extract emails
emails = [user["email"] for user in active_users]
print(f"Active emails: {emails}")

# Transform data structure
user_lookup = {user["id"]: user["name"] for user in users}
print(user_lookup)  # Output: {1: 'Alice', 2: 'Bob', 3: 'Charlie'}

Python's list comprehensions and dictionary operations make API data transformation concise and readable.

Error Handling for API Calls

Network requests can fail for many reasons. Implement comprehensive error handling:

def safe_api_call(json_response_string, expected_keys):
    try:
        data = json.loads(json_response_string)

        # Validate expected keys exist
        for key in expected_keys:
            if key not in data:
                raise KeyError(f"Missing required key: {key}")

        return data
    except json.JSONDecodeError as e:
        print(f"Invalid JSON format: {e}")
        return None
    except KeyError as e:
        print(f"Data validation error: {e}")
        return None

# Example usage
response = '{"temperature": 72, "conditions": "sunny"}'
weather = safe_api_call(response, ["temperature", "conditions"])
if weather:
    print(f"Temperature: {weather['temperature']}")

This pattern validates JSON format and ensures required fields exist before processing, preventing unexpected crashes.

WASM and Browser Considerations

When running Python in browsers (Pyodide/WASM), API interactions differ from traditional Python:

Browser Environment:

  • Use js module to access browser fetch API
  • All HTTP requests are asynchronous (require async/await)
  • CORS (Cross-Origin Resource Sharing) restrictions apply
  • No direct access to requests or urllib libraries

Traditional Python:

  • Use requests library for HTTP
  • Synchronous requests by default
  • No CORS restrictions for server-side code
  • Full access to HTTP libraries

For learning, understand both approaches. Browser-based Pyodide requires the js module integration, while server/desktop Python uses standard libraries. The core concepts—JSON parsing, status handling, data transformation—remain identical across environments.

Practical API Patterns

Building API Wrappers:

class WeatherAPI:
    def __init__(self, api_key):
        self.api_key = api_key

    def parse_weather(self, json_string):
        """Parse weather JSON response"""
        try:
            data = json.loads(json_string)
            return {
                "temperature": data["temperature"],
                "conditions": data["conditions"],
                "humidity": data.get("humidity", "N/A")
            }
        except (json.JSONDecodeError, KeyError) as e:
            print(f"Parse error: {e}")
            return None

# Usage
api = WeatherAPI("your-api-key")
response = '{"temperature": 68, "conditions": "cloudy", "humidity": 55}'
weather = api.parse_weather(response)
print(weather)

Wrapping API logic in classes organizes code and makes APIs easier to use throughout your application.

Try It Yourself: Practice Exercises

Exercise 1: JSON Parser
Create a function that takes a JSON string representing a book (title, author, year, pages) and returns a formatted string like "The Great Gatsby by F. Scott Fitzgerald (1925)".

Exercise 2: Status Code Handler
Write a function that takes a status code and returns a user-friendly message explaining what it means (200, 404, 401, 500).

Exercise 3: Data Filter
Given a JSON array of products with name, price, and category, write code to filter products under $50 and return their names.

Exercise 4: JSON Builder
Create a function that takes a list of dictionaries (users) and converts it to a prettified JSON string using json.dumps with indentation.

Exercise 5: API Response Validator
Write a function that validates an API response has required fields (status, data, message) and returns True/False with error messages.

Exercise 6: Nested JSON Navigator
Given a JSON string with nested user data (user -> address -> city), write code to safely extract the city name with proper error handling.

What's Next?

You've mastered Python API integration: understanding REST principles, working with JSON data, making HTTP requests (both traditional and WASM approaches), handling responses and status codes, and building robust API interactions. These skills let you connect programs to external services and build applications that leverage the vast ecosystem of web APIs.

In the next chapter, we'll explore testing and debugging—writing unit tests, using Python's debugger, implementing test-driven development, and building reliable, maintainable code through systematic testing practices.

Continue to Chapter 10: Testing and Debugging

📝 Test Your Knowledge: Working with APIs

Take this quiz to reinforce what you've learned in this chapter.