Day 19: Working with APIs & JSON Handling

🐍 Day 19: Working with APIs & JSON Handling


1. Learning Objectives

By the end of Day 19, you will be able to:

  • Understand what APIs are and how they enable programs to communicate over the web
  • Make HTTP requests using Python's requests library (GET, POST, PUT, DELETE)
  • Parse JSON responses and convert them into Python dictionaries
  • Handle query parameters, headers, and authentication
  • Manage API errors gracefully with status codes and exception handling
  • Build a simple API client that fetches and processes real-world data

2. Concept Explanation

2.1 What is an API? — Programs Talking to Programs

API stands for Application Programming Interface. It's a set of rules that allows one piece of software to talk to another. A REST API (the most common type on the web) lets you send HTTP requests to a URL and receive data back — usually in JSON format.

Think of an API like a waiter at a restaurant:

You (Client)Waiter (API)Kitchen (Server)
Order foodTakes order, delivers itCooks the food
You don't need to know how the kitchen works — you just get your meal.  

Similarly, you don't need to know how Twitter's database works — you just ask the API for tweets, and it returns them in a clean format.


2.2 HTTP Requests — The Language of the Web

Every time you visit a website, your browser makes an HTTP request. APIs use the same protocol:

HTTP MethodPurposeExample
GETRetrieve dataGet weather for a city
POSTSend new dataCreate a new user account
PUTUpdate existing dataEdit a blog post
DELETERemove dataDelete a comment

URL structure for APIs:

https://api.example.com/v1/users?page=2&limit=10
└──────┬──────┘ └─┬─┘ └──┬──┘ └──────┬────────┘
    Base URL    Version Endpoint  Query Parameters

2.3 Installing and Using requests

The requests library is the standard for HTTP in Python. It's not built-in, so install it first:

pip install requests

First request — a simple GET:

import requests

response = requests.get("https://api.github.com")
print(response.status_code)  # 200 means success
print(response.text[:200])   # First 200 characters of the response body

The response object has everything you need:

Attribute / MethodWhat It Returns
response.status_codeHTTP status code (200, 404, 500, etc.)
response.textResponse body as a string
response.json()Parsed JSON as a Python dict/list
response.headersResponse headers as a dict
response.okTrue if status code is 200–399
response.raise_for_status()Raises an exception if the request failed

2.4 JSON — The Language of APIs

JSON (JavaScript Object Notation) is the universal data format for APIs. It looks almost identical to Python dictionaries and lists:

{
    "name": "Alice",
    "age": 28,
    "skills": ["Python", "JavaScript"],
    "address": {
        "city": "Manila",
        "country": "Philippines"
    }
}

Python ↔ JSON conversion:

import json

# Python dict → JSON string (serialisation)
data = {"name": "Alice", "age": 28}
json_string = json.dumps(data, indent=2)
print(json_string)

# JSON string → Python dict (deserialisation)
python_dict = json.loads(json_string)
print(python_dict["name"])  # Alice

With requests, you rarely need json.loads() directly — just use .json():

response = requests.get("https://api.github.com/users/octocat")
user_data = response.json()        # Already a Python dict!
print(user_data["login"])          # octocat
print(user_data["public_repos"])   # 8

2.5 Working with a Real API — Practice with JSONPlaceholder

JSONPlaceholder is a free fake API for testing. Let's explore it:

import requests

BASE_URL = "https://jsonplaceholder.typicode.com"

# GET all posts
response = requests.get(f"{BASE_URL}/posts")
posts = response.json()
print(f"Fetched {len(posts)} posts")
print(f"First post title: {posts[0]['title']}")

# GET a single post by ID
response = requests.get(f"{BASE_URL}/posts/1")
post = response.json()
print(f"Post 1: {post['title']}")

# POST — create a new post
new_post = {"title": "Learning Python", "body": "APIs are fun!", "userId": 1}
response = requests.post(f"{BASE_URL}/posts", json=new_post)
created = response.json()
print(f"Created post ID: {created['id']}")

# Query parameters — filter posts by user
response = requests.get(f"{BASE_URL}/posts", params={"userId": 1})
user1_posts = response.json()
print(f"User 1 has {len(user1_posts)} posts")

2.6 Handling Headers and Authentication

Many APIs require authentication. Common methods:

API Key in query parameter:

response = requests.get(
    "https://api.weatherapi.com/v1/current.json",
    params={"key": "YOUR_API_KEY", "q": "Manila"}
)

API Key in header:

headers = {"Authorization": "Bearer YOUR_TOKEN_HERE"}
response = requests.get("https://api.example.com/data", headers=headers)

Basic authentication:

from requests.auth import HTTPBasicAuth
response = requests.get(
    "https://api.example.com/secure",
    auth=HTTPBasicAuth("username", "password")
)

2.7 Error Handling for API Requests

Network calls can fail for many reasons. Always handle them:

import requests

def fetch_data(url):
    """Safely fetch data from an API."""
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()  # Raises HTTPError for 4xx/5xx
        return response.json()
    except requests.exceptions.Timeout:
        print("Request timed out. Check your network.")
    except requests.exceptions.ConnectionError:
        print("Could not connect to the server.")
    except requests.exceptions.HTTPError as e:
        print(f"HTTP error: {e}")
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
    return None

data = fetch_data("https://jsonplaceholder.typicode.com/posts")
if data:
    print(f"Got {len(data)} items")

Common HTTP status codes:

CodeMeaning
200OK — request succeeded
201Created — new resource made
204No Content — success, nothing to return
400Bad Request — your fault
401Unauthorized — log in first
403Forbidden — you don't have permission
404Not Found — resource doesn't exist
500Internal Server Error — their fault
503Service Unavailable — try again later

2.8 Building Reusable API Clients

Wrap API calls in a class for clean, maintainable code:

class JSONPlaceholderClient:
    """A reusable client for the JSONPlaceholder API."""

    def __init__(self):
        self.base_url = "https://jsonplaceholder.typicode.com"

    def get_posts(self, user_id=None):
        """Get all posts, optionally filtered by user_id."""
        params = {"userId": user_id} if user_id else {}
        response = requests.get(f"{self.base_url}/posts", params=params)
        response.raise_for_status()
        return response.json()

    def get_post(self, post_id):
        """Get a single post by ID."""
        response = requests.get(f"{self.base_url}/posts/{post_id}")
        response.raise_for_status()
        return response.json()

    def create_post(self, title, body, user_id=1):
        """Create a new post."""
        data = {"title": title, "body": body, "userId": user_id}
        response = requests.post(f"{self.base_url}/posts", json=data)
        response.raise_for_status()
        return response.json()

# Usage
client = JSONPlaceholderClient()
posts = client.get_posts(user_id=1)
print(f"User 1 posts: {len(posts)}")

3. Code Examples

Example 1: Weather Fetcher (Simulated)

import requests

def get_weather(city):
    """Fetch weather for a city using a free API.

    Note: You'll need a free API key from https://openweathermap.org/api
    This example uses a simulated response if no key is available.
    """
    API_KEY = "YOUR_API_KEY"  # Replace with a real key
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {"q": city, "appid": API_KEY, "units": "metric"}

    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
        temp = data["main"]["temp"]
        desc = data["weather"][0]["description"]
        return f"{city}: {temp}°C, {desc}"
    except requests.exceptions.RequestException as e:
        return f"Weather fetch failed: {e}"

# print(get_weather("Manila"))

Example 2: GitHub User Info

import requests

def get_github_user(username):
    """Fetch public information about a GitHub user."""
    url = f"https://api.github.com/users/{username}"
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    user = response.json()
    return {
        "name": user.get("name", "N/A"),
        "repos": user["public_repos"],
        "followers": user["followers"],
        "avatar": user["avatar_url"],
    }

info = get_github_user("octocat")
print(f"Name: {info['name']}")
print(f"Public repos: {info['repos']}")
print(f"Followers: {info['followers']}")

Example 3: POST with Error Handling

import requests

def create_comment(post_id, name, email, body):
    """Post a comment to JSONPlaceholder."""
    url = f"https://jsonplaceholder.typicode.com/posts/{post_id}/comments"
    data = {"name": name, "email": email, "body": body}

    try:
        response = requests.post(url, json=data, timeout=10)
        response.raise_for_status()
        created = response.json()
        print(f"Comment created with ID: {created['id']}")
        return created
    except requests.exceptions.HTTPError as e:
        print(f"Server rejected comment: {e}")
    except requests.exceptions.RequestException as e:
        print(f"Network error: {e}")
    return None

create_comment(1, "Alice", "alice@example.com", "Great post!")

4. Hands-On Exercises

Exercise 1: Fetch and Display

Using the JSONPlaceholder API (/posts), fetch all posts and display each post's title and body (first 80 characters only). Format the output nicely.

Exercise 2: Filter by User

Ask the user for a user ID (1–10). Fetch all posts by that user from JSONPlaceholder. Print the number of posts and list their titles.

Exercise 3: JSON File Merger

Create a Python script that:

  • Fetches posts from JSONPlaceholder
  • Saves them to posts.json
  • Then reads posts.json back into Python and prints the number of posts loaded

Exercise 4: Error-Resistant API Caller

Write a function safe_get(url) that takes any URL, makes a GET request, and returns the JSON. Handle Timeout, ConnectionError, and HTTPError. Return None on any failure.

Exercise 5: Headers Inspector

Make a GET request to https://httpbin.org/headers with a custom header X-Learning: Python. Print the response JSON to see what the server received. Verify your custom header was sent.


5. Applied Challenge Task πŸ—️

GitHub Profile Analyzer CLI

Build a command-line tool that analyzes a GitHub user's public profile using the GitHub API.

Requirements:

  1. Ask for a GitHub username at the command line.

  2. Fetch the user's profile from https://api.github.com/users/{username}.

  3. Fetch their public repositories from https://api.github.com/users/{username}/repos.

  4. Display a profile summary:

    === GITHUB PROFILE: octocat ===
    Name:        The Octocat
    Bio:         (if available)
    Public Repos: 8
    Followers:    9876
    Following:    2
    ==============================
    
  5. Display repository statistics:

    • Total number of repos
    • Most starred repo (name + stars)
    • Most used language (count them up)
    • Average repo size
  6. Error handling:

    • If the user doesn't exist, show a friendly message.
    • If rate-limited (GitHub allows 60 requests/hour unauthenticated), inform the user.
    • Handle network errors gracefully.
  7. Optional — cache results: Save analysis to a JSON file so repeated queries for the same user are instant.

Example repository language counter:

languages = {}
for repo in repos:
    lang = repo.get("language")
    if lang:
        languages[lang] = languages.get(lang, 0) + 1
most_used = max(languages, key=languages.get)

Stretch goals:

  • Add an option to export the report as a formatted text file.
  • Use the datetime module to show when the user was created and last active.
  • Add a simple rate-limit tracker.

6. Brief Review Summary

ConceptKey Points
APIA set of rules for programs to communicate over HTTP
JSONUniversal data format; looks like Python dicts and lists
requests.get()Makes a GET request; returns a Response object
.json()Parses response body into Python dict/list
.raise_for_status()Raises exception for 4xx/5xx status codes
Query parametersUse params= dict for GET; json= dict for POST
Error handlingCatch Timeout, ConnectionError, HTTPError
API Client classEncapsulates API logic for reusability

7. Preview of Next Topic — Day 20

Tomorrow we enter the world of concurrent programming:

  • Multithreading — running multiple tasks in parallel within a single process
  • Multiprocessing — spreading work across multiple CPU cores
  • The threading and multiprocessing modules
  • When to use each — the GIL explained
  • Real‑world use cases: web scraping, CPU‑bound work, I/O‑bound work

🎯 Your Action Items for Day 19:

  1. ✅ Complete all 5 exercises
  2. ✅ Build the GitHub Profile Analyzer CLI
  3. ✅ Explore a public API of your choice (PokΓ©mon, Star Wars, weather)
  4. ✅ Practice writing API calls that handle all error conditions gracefully

Comments

Popular posts from this blog

Day 1: Welcome to Python — Your Journey Begins

Python: Your Gateway to Coding Adventures

Day 11: List Comprehensions & Lambda Functions

Earn From the Comfort of Your Home

Build the skills to work comfortably from home, on your own terms.

Get Started Today