0%

67- APIs & JSON

Connect to web services, fetch data from APIs, and work with JSON. The modern web speaks JSON. Learn its language.

The web is not just HTML pages. Many websites provide APIs (Application Programming Interfaces) that allow programs to access data directly. Weather forecasts, stock prices, news articles, social media posts, maps, translations; all available through APIs.
APIs speak JSON. JavaScript Object Notation is a lightweight, human-readable format for structuring data. It looks like Python dictionaries and lists. And that is no coincidence.
This lesson covers everything you need to work with APIs and JSON. You will learn to parse JSON into Python objects, convert Python objects to JSON, make API requests, handle authentication, and process JSON responses. These skills are essential for modern programming.

🕯️ Magic Note

JSON (JavaScript Object Notation) has become the universal language of web APIs. It is language-independent but looks almost identical to Python dictionaries and lists. This is why Python is so popular for API integration, the translation is almost seamless.

What is JSON?
JSON is a text format for storing and exchanging data.

JSON

{

“name”: “Feloriya”,

“age”: 25,

“skills”: [“Python”, “Web Design”, “SEO”],

“is_active”: true,

“address”: {

“city”: “Tehran”,

“zip”: “12345”

},

“score”: null

}

🕯️ Magic Note

JSON supports: strings (in double quotes), numbers, booleans (true/false), null, arrays (like Python lists), and objects (like Python dictionaries). Notice that booleans are lowercase (true/false), not capitalized like Python’s True/False.

JSON vs Python: Type Mapping
JSON types map cleanly to Python types.
JSON TypePython Type
object ({}),dict
array ([]),list
string,str
number (integer),int
number (float),float
true,True
false,False
null,None

Python

import json

# Python dictionary (matches JSON structure)

data = {

“name”: “Feloriya”,

“age”: 25,

“skills”: [“Python”, “Web Design”, “SEO”],

“is_active”: True,

“address”: {

“city”: “Tehran”,

“zip”: “12345”

},

“score”: None

}

Parsing JSON: json.loads() and json.load()
Convert JSON strings or files into Python objects.

Python

import json

# Parse JSON string to Python (json.loads)

json_string = ‘{“name”: “Ali”, “age”: 25, “city”: “Tehran”}’

data = json.loads(json_string)

print(data[“name”]) # Ali

print(type(data)) # <class ‘dict’>

# Parse JSON array

json_array = ‘[{“name”: “Ali”}, {“name”: “Sara”}]’

users = json.loads(json_array)

print(users[0][“name”]) # Ali

# Parse JSON from file (json.load)

with open(“data.json”, “r”, encoding=”utf-8″) as f:

data = json.load(f)

print(data[“name”])

💡 Use json.loads() for JSON strings (load from string). Use json.load() for JSON files (load from file). The ‘s’ stands for string.
Serializing to JSON: json.dumps() and json.dump()
Convert Python objects to JSON strings or files.

Python

import json

data = {

“name”: “Feloriya”,

“age”: 25,

“skills”: [“Python”, “Web Design”],

“is_active”: True,

“score”: None

}

# Convert to JSON string (json.dumps)

json_string = json.dumps(data)

print(json_string)

# {“name”: “Feloriya”, “age”: 25, “skills”: [“Python”, “Web Design”], “is_active”: true, “score”: null}

# Pretty print with indentation

json_string_pretty = json.dumps(data, indent=2, sort_keys=True)

print(json_string_pretty)

# Write JSON to file (json.dump)

with open(“output.json”, “w”, encoding=”utf-8″) as f:

json.dump(data, f, indent=2, ensure_ascii=False)

🕯️ Magic Note

The indent parameter creates human-readable JSON. The sort_keys parameter sorts dictionary keys alphabetically. The ensure_ascii=False allows non-ASCII characters (like Persian) to remain unescaped.

Making API Requests with requests
Combine JSON parsing with HTTP requests to fetch data from APIs.

Python

import requests

import json

# Fetch data from a public API

response = requests.get(“https://api.github.com/users/octocat”)

if response.status_code == 200:

user_data = response.json() # Parse JSON directly

print(f”Name: {user_data.get(‘name’, ‘N/A’)}”)

print(f”Followers: {user_data.get(‘followers’, 0)}”)

print(f”Repos: {user_data.get(‘public_repos’, 0)}”)

else:

print(f”Error: {response.status_code}”)

# Alternative: parse response text manually

if response.status_code == 200:

user_data = json.loads(response.text)

print(user_data[“login”])

💡 The response.json() method is a shortcut for json.loads(response.text). It is the preferred way to parse JSON responses from APIs.
Working with API Query Parameters
Pass parameters to filter, sort, or paginate API results.

Python

import requests

# Search GitHub repositories

params = {

“q”: “python”,

“sort”: “stars”,

“order”: “desc”,

“per_page”: 5

}

response = requests.get(“https://api.github.com/search/repositories”, params=params)

if response.status_code == 200:

data = response.json()

print(f”Total results: {data[‘total_count’]}”)

for repo in data.get(“items”, []):

print(f” – {repo[‘name’]}: {repo[‘stargazers_count’]} stars”)

API Authentication (API Keys, Tokens)
Many APIs require authentication. Common methods include API keys and bearer tokens.

Python

import requests

import os

# Method 1: API Key as query parameter

params = {“api_key”: “YOUR_API_KEY”}

response = requests.get(“https://api.example.com/data”, params=params)

# Method 2: API Key in header

headers = {“X-API-Key”: “YOUR_API_KEY”}

response = requests.get(“https://api.example.com/data”, headers=headers)

# Method 3: Bearer token (JWT, OAuth)

headers = {“Authorization”: “Bearer YOUR_TOKEN”}

response = requests.get(“https://api.example.com/user”, headers=headers)

# Method 4: Basic authentication (username/password)

response = requests.get(“https://api.example.com/private”, auth=(“username”, “password”))

# Best practice: Store keys in environment variables

API_KEY = os.environ.get(“API_KEY”)

headers = {“Authorization”: f”Bearer {API_KEY}”}

⚠️ Never hard-code API keys in your source code. Use environment variables or a .env file with python-dotenv. Add the file to .gitignore to avoid committing secrets.
Posting Data to an API
Send JSON data to create or update resources on the server.

Python

import requests

# POST JSON data

new_post = {

“title”: “My First Post”,

“body”: “This is the content of my post.”,

“userId”: 1

}

response = requests.post(

“https://jsonplaceholder.typicode.com/posts”,

json=new_post, # Automatically serializes to JSON

headers={“Content-Type”: “application/json”}

)

if response.status_code == 201: # Created

created = response.json()

print(f”Post created with ID: {created[‘id’]}”)

else:

print(f”Error: {response.status_code}”)

# PUT request (update entire resource)

updated_data = {“title”: “Updated Title”, “body”: “Updated content”, “userId”: 1}

response = requests.put(“https://jsonplaceholder.typicode.com/posts/1”, json=updated_data)

# PATCH request (partial update)

response = requests.patch(“https://jsonplaceholder.typicode.com/posts/1”, json={“title”: “New Title”})

# DELETE request

response = requests.delete(“https://jsonplaceholder.typicode.com/posts/1”)

print(f”Delete status: {response.status_code}”)

🕯️ Magic Note

When you use the json parameter in requests.post(), the library automatically sets Content-Type: application/json and serializes your Python dictionary to JSON. Do not manually call json.dumps() unless you need special control.

Handling API Errors and Rate Limiting
APIs can fail or limit how many requests you can make. Handle errors gracefully.

Python

import requests

import time

def safe_api_call(url, params=None, max_retries=3, delay=1):

for attempt in range(max_retries):

try:

response = requests.get(url, params=params, timeout=10)

if response.status_code == 200:

return response.json()

elif response.status_code == 429: # Too Many Requests

wait_time = int(response.headers.get(“Retry-After”, delay))

print(f”Rate limited. Waiting {wait_time} seconds…”)

time.sleep(wait_time)

elif response.status_code >= 500: # Server errors

print(f”Server error {response.status_code}. Retrying…”)

time.sleep(delay * (attempt + 1))

else:

print(f”Error {response.status_code}: {response.text}”)

return None

except requests.exceptions.Timeout:

print(f”Timeout on attempt {attempt + 1}”)

time.sleep(delay * (attempt + 1))

except requests.exceptions.RequestException as e:

print(f”Request failed: {e}”)

time.sleep(delay * (attempt + 1))

print(“Max retries exceeded”)

return None

Practical Example: Weather API Client
Fetch weather data from a public API and display it nicely.

Python

import requests

from datetime import datetime

def get_weather(city, api_key):

“””Fetch current weather for a city.”””

base_url = “https://api.openweathermap.org/data/2.5/weather”

params = {

“q”: city,

“appid”: api_key,

“units”: “metric”, # Celsius

“lang”: “en”

}

try:

response = requests.get(base_url, params=params, timeout=10)

response.raise_for_status()

data = response.json()

return {

“city”: data[“name”],

“country”: data[“sys”][“country”],

“temperature”: data[“main”][“temp”],

“feels_like”: data[“main”][“feels_like”],

“humidity”: data[“main”][“humidity”],

“pressure”: data[“main”][“pressure”],

“description”: data[“weather”][0][“description”],

“wind_speed”: data[“wind”][“speed”],

“sunrise”: datetime.fromtimestamp(data[“sys”][“sunrise”]).strftime(“%H:%M”),

“sunset”: datetime.fromtimestamp(data[“sys”][“sunset”]).strftime(“%H:%M”)

}

except requests.exceptions.RequestException as e:

print(f”Error fetching weather: {e}”)

return None

def display_weather(weather):

if not weather:

print(“No weather data available”)

return

print(“=” * 50)

print(f”Weather in {weather[‘city’]}, {weather[‘country’]}”)

print(“=” * 50)

print(f”Temperature: {weather[‘temperature’]:.1f}°C (feels like {weather[‘feels_like’]:.1f}°C)”)

print(f”Condition: {weather[‘description’].capitalize()}”)

print(f”Humidity: {weather[‘humidity’]}%”)

print(f”Pressure: {weather[‘pressure’]} hPa”)

print(f”Wind Speed: {weather[‘wind_speed’]} m/s”)

print(f”Sunrise: {weather[‘sunrise’]}, Sunset: {weather[‘sunset’]}”)

# Usage

# API_KEY = os.environ.get(“OPENWEATHER_API_KEY”)

# weather = get_weather(“Tehran”, API_KEY)

# display_weather(weather)

Practical Example: REST API Client for a Task Manager
Build a simple client for a RESTful task management API.

Python

import requests

class TaskAPIClient:

def __init__(self, base_url, api_key=None):

self.base_url = base_url.rstrip(“/”)

self.session = requests.Session()

if api_key:

self.session.headers.update({“Authorization”: f”Bearer {api_key}”})

def get_tasks(self, completed=None):

“””Get all tasks, optionally filtered by completion status.”””

params = {}

if completed is not None:

params[“completed”] = str(completed).lower()

response = self.session.get(f”{self.base_url}/tasks”, params=params)

response.raise_for_status()

return response.json()

def create_task(self, title, description=None):

“””Create a new task.”””

task_data = {“title”: title}

if description:

task_data[“description”] = description

response = self.session.post(f”{self.base_url}/tasks”, json=task_data)

response.raise_for_status()

return response.json()

def get_task(self, task_id):

“””Get a single task by ID.”””

response = self.session.get(f”{self.base_url}/tasks/{task_id}”)

response.raise_for_status()

return response.json()

def update_task(self, task_id, **kwargs):

“””Update a task (title, description, completed).”””

response = self.session.patch(f”{self.base_url}/tasks/{task_id}”, json=kwargs)

response.raise_for_status()

return response.json()

def delete_task(self, task_id):

“””Delete a task.”””

response = self.session.delete(f”{self.base_url}/tasks/{task_id}”)

response.raise_for_status()

return response.status_code == 204

# Usage example

# client = TaskAPIClient(“https://api.example.com”)

# tasks = client.get_tasks(completed=False)

# new_task = client.create_task(“Learn Python APIs”, “Study requests and JSON”)

Common Mistakes with APIs and JSON
  • Forgetting to handle HTTP errors (always check response.status_code)
  • Not setting timeouts on requests (requests can hang forever)
  • Hard-coding API keys in source code (use environment variables)
  • Assuming response.json() always works (handle JSONDecodeError)
  • Ignoring rate limits (getting your IP banned)
  • Not validating JSON before parsing (malformed JSON raises exception)
Check Your Understanding
  • How do you parse a JSON string into a Python dictionary?
  • Write code to fetch data from a public API and parse the JSON response.
  • What is the difference between json.load() and json.loads()?
  • How do you send JSON data in a POST request?
  • What is the purpose of the indent parameter in json.dumps()?
  • How should you handle API authentication keys in production code?

⚡ Whisper

APIs are the pipes of the modern web. They connect applications to data. JSON is the language they speak. With requests you ask. With json you understand. A GET request fetches data. A POST request sends data. A response comes back in JSON. You parse it with .json(). Suddenly, JSON becomes Python. Lists become lists. Dictionaries become dictionaries. Strings become strings. The translation is seamless. You navigate the data with brackets and keys. data[“user”][“name”]. data[“items”][0][“price”]. This is not magic. This is data exchange. Learn the patterns. Handle errors. Respect rate limits. Secure your keys. The API world is vast. Weather, maps, payments, social media, AI. All waiting for your request. Choose an API. Fetch the data. Parse the JSON. Build something new. The web is open. Ask your questions.

Related posts