PYTHON PROGRAMMING:Day 22: Working with APIs

Mastering day 22: working with apis concepts and implementation.

On Day 21 you built a Flask app that serves data to browsers — your Python code was the server. But most modern applications also consume data from other services. Your weather app does not calculate forecasts itself; it asks a weather API. Your payment page does not connect to banks directly; it calls a payment API.

An API (Application Programming Interface) is a contract between two programs: "if you send me a request in this format, I will send you a response in that format." You have already written a tiny API with Flask's /api/data route. Today you will be on the other side — the client making requests to someone else's API.

What You Will Learn in This Chapter

By the end of this tutorial you will be able to:

  • Explain what an API is using real-world analogies
  • Understand REST APIs and HTTP methods (GET, POST, PUT, DELETE)
  • Make HTTP requests with Python's requests library
  • Parse and create JSON data
  • Handle API errors gracefully
  • Work with query parameters and request headers
  • Understand authentication and rate limiting at a beginner level
  • Avoid common API mistakes

Estimated time: 55 minutes reading + 25 minutes practice

What Is an API? The Waiter Analogy

Imagine you are at a restaurant. You do not walk into the kitchen and cook your own meal. Instead, you tell the waiter what you want. The waiter takes your order to the kitchen, the kitchen prepares the food, and the waiter brings it back to your table.

An API is the waiter between your program and another service:

  • You (the client) — your Python script
  • The waiter (the API) — accepts your request, translates it, delivers it
  • The kitchen (the server) — the other company's database and logic

You never need to know how the kitchen works. You just need to know the menu — which "dishes" (endpoints) are available and what "orders" (requests) they accept.

Your Python code  →  HTTP Request  →  API Server  →  Database
Your Python code  ←  HTTP Response ←  API Server  ←  Database

REST APIs — The Most Common Style

REST (Representational State Transfer) is the dominant API style on the web. REST APIs use standard HTTP methods on URL "resources":

MethodActionExample URLWhat it does
GETRead`/users/42`Fetch user 42's profile
POSTCreate`/users`Create a new user
PUTUpdate`/users/42`Replace user 42's data
DELETEDelete`/users/42`Remove user 42

Each URL is a resource — a thing (user, product, weather report) identified by its address. The HTTP method tells the server what you want to do with that resource.

Most APIs return data as JSON — the same format Flask's jsonify() produces on Day 21.

Setting Up the requests Library

Python's built-in urllib can make HTTP requests, but the third-party requests library is simpler and more popular:

pip install requests

Basic usage:

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
print(response.status_code)    # 200 (success)
print(response.json())         # parsed JSON as a Python dict

Output:

200
{'userId': 1, 'id': 1, 'title': '...', 'body': '...'}

Three lines — request, status check, data. That simplicity is why requests is the standard choice.

Making GET Requests — Fetching Data

GET requests retrieve data without changing anything on the server. Pass extra information via query parameters in the URL:

import requests

# GET with query parameters
url = "https://api.openweathermap.org/data/2.5/weather"
params = {
    'q': 'London',
    'appid': 'your_api_key_here',   # replace with a real key
    'units': 'metric'
}

response = requests.get(url, params=params)
print(response.url)
# https://api.openweathermap.org/data/2.5/weather?q=London&appid=...&units=metric

requests builds the full URL for you from the base URL and the params dictionary.

Here is a complete function with error handling:

import requests

def get_weather(city):
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {
        'q': city,
        'appid': 'your_api_key_here',
        'units': 'metric'
    }

    try:
        response = requests.get(url, params=params)
        response.raise_for_status()

        data = response.json()
        return {
            'city': data['name'],
            'temperature': data['main']['temp'],
            'description': data['weather'][0]['description']
        }
    except requests.exceptions.RequestException as e:
        return {'error': str(e)}

result = get_weather("London")
print(result)

Walkthrough:

  1. requests.get() sends the HTTP GET request.
  2. response.raise_for_status() raises an exception if the status code is 4xx or 5xx (client or server error).
  3. response.json() parses the JSON body into a Python dictionary.
  4. We extract nested fields (data['main']['temp']) and return a clean result.
  5. The except block catches network errors, timeouts, and bad status codes — returning an error dict instead of crashing.

Making POST Requests — Sending Data

POST requests send data to the server — creating a new record, submitting a form, uploading a file:

import requests

def create_user(user_data):
    url = "https://jsonplaceholder.typicode.com/users"

    try:
        response = requests.post(url, json=user_data)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        return {'error': str(e)}

new_user = {
    'name': 'Priya Sharma',
    'email': 'priya@schoolabe.com',
    'username': 'priya_s'
}

result = create_user(new_user)
print(result)

The json= parameter tells requests to:

  1. Convert the Python dict to a JSON string
  2. Set the Content-Type: application/json header automatically

Without json=, you would need to call json.dumps() yourself and set headers manually.

Working with JSON in Python

JSON maps directly to Python types:

JSONPython
object `{}``dict`
array `[]``list`
string`str`
number`int` or `float`
true/false`True`/`False`
null`None`
import json

# JSON string → Python object
json_data = '''
{
    "users": [
        {"id": 1, "name": "Priya", "email": "priya@schoolabe.com"},
        {"id": 2, "name": "Marcus", "email": "marcus@schoolabe.com"}
    ]
}
'''

data = json.loads(json_data)

for user in data['users']:
    print(f"User: {user['name']} ({user['email']})")

Output:

User: Priya (priya@schoolabe.com)
User: Marcus (marcus@schoolabe.com)

Python object → JSON string:

new_user = {"id": 3, "name": "Sofia", "email": "sofia@schoolabe.com"}
json_string = json.dumps(new_user, indent=2)
print(json_string)

Output:

{
  "id": 3,
  "name": "Sofia",
  "email": "sofia@schoolabe.com"
}
  • json.loads()load a JSON string into Python
  • json.dumps()dump a Python object into a JSON string
  • response.json() — shortcut on a requests response object

Understanding HTTP Status Codes

Every API response includes a status code. You should recognize the common ones:

CodeMeaningWhat to do
200OKSuccess — process the data
201CreatedPOST succeeded — new resource created
400Bad RequestCheck your request format/parameters
401UnauthorizedMissing or invalid API key
404Not FoundWrong URL or resource does not exist
429Too Many RequestsSlow down — you hit the rate limit
500Server ErrorProblem on their end — retry later
response = requests.get("https://jsonplaceholder.typicode.com/posts/99999")

if response.status_code == 200:
    print(response.json())
elif response.status_code == 404:
    print("Post not found")
else:
    print(f"Unexpected error: {response.status_code}")

Or use raise_for_status() inside a try/except block to handle all error codes at once — as shown in the weather example above.

Authentication — Proving Who You Are

Many APIs require an API key or token to identify you and track usage:

headers = {
    'Authorization': 'Bearer your_token_here',
    'Accept': 'application/json'
}

response = requests.get(url, headers=headers)

Some APIs pass the key as a query parameter (?appid=your_key), others require it in a header. Always read the API's documentation — each service has its own rules.

Security rule: Never hardcode API keys in source code that gets shared or committed to Git. Use environment variables:

import os

api_key = os.environ.get('WEATHER_API_KEY')

Rate Limiting — Do Not Overwhelm the Server

APIs often limit how many requests you can make per minute or per day. If you exceed the limit, you get a 429 Too Many Requests response.

Good practices:

  • Cache responses when data does not change often (weather every 30 minutes, not every second)
  • Add delays between requests when fetching many pages
  • Read the API's rate limit documentation before building
import time
import requests

urls = [f"https://jsonplaceholder.typicode.com/posts/{i}" for i in range(1, 6)]

for url in urls:
    response = requests.get(url)
    print(f"Post {response.json()['id']}: {response.json()['title'][:40]}...")
    time.sleep(0.5)   # polite pause between requests

A Complete API Client Example

import requests
import json

def get_weather(city):
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {
        'q': city,
        'appid': 'your_api_key_here',
        'units': 'metric'
    }
    try:
        response = requests.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        return {
            'city': data['name'],
            'temperature': data['main']['temp'],
            'description': data['weather'][0]['description']
        }
    except requests.exceptions.RequestException as e:
        return {'error': str(e)}

def create_user(user_data):
    url = "https://jsonplaceholder.typicode.com/users"
    try:
        response = requests.post(url, json=user_data)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        return {'error': str(e)}

def process_json_data():
    json_data = '''
    {
        "users": [
            {"id": 1, "name": "Priya", "email": "priya@schoolabe.com"},
            {"id": 2, "name": "Marcus", "email": "marcus@schoolabe.com"}
        ]
    }
    '''
    data = json.loads(json_data)

    for user in data['users']:
        print(f"User: {user['name']} ({user['email']})")

    new_user = {"id": 3, "name": "Sofia", "email": "sofia@schoolabe.com"}
    json_string = json.dumps(new_user, indent=2)
    print("\nNew user JSON:")
    print(json_string)

print("Processing JSON data:")
process_json_data()

Output:

Processing JSON data:
User: Priya (priya@schoolabe.com)
User: Marcus (marcus@schoolabe.com)

New user JSON:
{
  "id": 3,
  "name": "Sofia",
  "email": "sofia@schoolabe.com"
}

Common Mistakes

Mistake 1: Not checking the status code

# Dangerous — .json() may fail on error pages
data = requests.get(url).json()

# Safe
response = requests.get(url)
response.raise_for_status()
data = response.json()

Mistake 2: Confusing json= and data= in POST requests

# Sends JSON (Content-Type: application/json) — most REST APIs expect this
requests.post(url, json={'name': 'Priya'})

# Sends form data (Content-Type: application/x-www-form-urlencoded) — HTML forms use this
requests.post(url, data={'name': 'Priya'})

Using the wrong one causes the server to reject or misparse your request.

Mistake 3: Hardcoding API keys in source code

# BAD — visible in Git history forever
API_KEY = "sk-abc123secret"

# GOOD — read from environment
import os
API_KEY = os.environ.get('API_KEY')

Mistake 4: Ignoring timeouts

# Can hang forever if the server is down
response = requests.get(url)

# Fails fast after 10 seconds
response = requests.get(url, timeout=10)

Always set a timeout so your program does not freeze on a dead server.

Mistake 5: Assuming the response structure never changes

APIs evolve. Wrap field access in try/except or use .get() with defaults:

# Crashes if 'main' key is missing
temp = data['main']['temp']

# Safe
temp = data.get('main', {}).get('temp', 'unknown')

Practice Exercises

Exercise 1: Use requests.get() to fetch post #1 from https://jsonplaceholder.typicode.com/posts/1 and print its title.

Exercise 2: Fetch all posts (/posts) and print the titles of the first five.

Exercise 3: POST a new todo item to https://jsonplaceholder.typicode.com/todos with {"title": "Learn APIs", "completed": false, "userId": 1}.

Exercise 4: Write a function that takes a URL, makes a GET request with a 5-second timeout, and returns either the JSON data or an error message string.

See all Python practice exercises with solutions

What Comes Next — Day 23: Database Operations

APIs move data between programs over the network. But where does data live permanently? Databases — organized storage that survives after your program closes. Day 23 teaches you to create, read, update, and delete records with Python and SQLite.

Day 23 covers:

  • What databases are and why apps need them
  • SQL basics (SELECT, INSERT, UPDATE, DELETE)
  • Working with SQLite using Python's built-in sqlite3 module
  • CRUD operations and safe parameterized queries

Continue to Day 23: Database Operations

Chapter navigation