Web APIs and JSON

Part of the free LogicWiz Python course, Module 15: Real-world Python. The examples and practice run in the app.

Shankar bhaiya changes the menu every morning on the canteen website, and Aarav retypes it into the app by hand. Why not ask the website for the menu directly? That is what an API is for.

You'll learn to

  • Call a web API with `requests.get` and `requests.post`, with `params` and `json`
  • Read status codes, and use `raise_for_status()` and `timeout` the right way
  • Turn JSON into Python objects and back with `.json()`, `json.loads` and `json.dumps`

Learn

What an API call is

An API is a website for programs. Your code sends a request to a URL, and the server sends back a response: a status code plus a body, usually JSON text.

Code Meaning
200 OK, here is your data
201 Created (after a POST)
400 Bad request: you sent something wrong
404 Not found
500, 503 The server broke or is busy

2xx is success, 4xx is your mistake, 5xx is theirs. In this course, requests talks to a demo server at https://api.logicwiz.ai/demo with the canteen's /menu, /weather, /rates, /orders and /status/<code>.

GET the menu

import requests

BASE = "https://api.logicwiz.ai/demo"

r = requests.get(BASE + "/menu", timeout=5)
print(r.status_code, r.ok)
print(r.headers["Content-Type"])

menu = r.json()            # JSON text -> a Python list of dicts
print(type(menu), len(menu), "items")
for dish in menu:
    print(f"{dish['id']}. {dish['item']:<14} Rs {dish['price']}")

r.json() parses the body for you. r.text is the same body as a plain string.

Query parameters with params=

import requests

BASE = "https://api.logicwiz.ai/demo"

r = requests.get(BASE + "/menu", params={"veg": "true", "max_price": 20}, timeout=5)
print(r.url)
print([d["item"] for d in r.json()])

r = requests.get(BASE + "/weather", params={"city": "Bengaluru"}, timeout=5)
print(r.json())

params= builds the ?veg=true&max_price=20 part of the URL safely, including spaces and symbols. Don't glue it together by hand.

When it fails: 404 and raise_for_status()

import requests

BASE = "https://api.logicwiz.ai/demo"

r = requests.get(BASE + "/menu/9", timeout=5)
print(r.status_code, r.ok, r.json())     # no exception yet!

try:
    r.raise_for_status()
except requests.HTTPError as e:
    print("HTTPError:", e)

A 404 is a normal answer, not a crash. requests only raises by itself when the network fails. Call raise_for_status() to turn 4xx/5xx into an HTTPError.

Always set a timeout

import requests

BASE = "https://api.logicwiz.ai/demo"

try:
    r = requests.get(BASE + "/slow", timeout=3)
    print(r.json())
except requests.Timeout:
    print("Server too slow, showing yesterday's menu instead")
except requests.RequestException as e:
    print("Network problem:", e)

Without timeout=, a real requests.get can wait forever on a stuck server and freeze your app. requests.RequestException is the parent of Timeout, ConnectionError and HTTPError, so it catches them all.

POST an order with json=

import requests

BASE = "https://api.logicwiz.ai/demo"

r = requests.post(BASE + "/orders", json={"item_id": 1, "qty": 3}, timeout=5)
print(r.status_code)
order = r.json()
print(order)
print(f"Order {order['order_id']}: {order['qty']} x {order['item']} = Rs {order['total']}")

bad = requests.post(BASE + "/orders", json={"item_id": 1}, timeout=5)
print(bad.status_code, bad.json()["error"])

json= turns your dict into JSON text and sets the right header. GET asks for data; POST sends data to create something (201 = Created).

Python and JSON are close cousins

PythonJSON text
dict {"a": 1}object {"a": 1}
list or tuplearray [1, 2]
True / Falsetrue / false
Nonenull
json.dumps(obj) → textjson.loads(text) → object
json.dump(obj, f) → filejson.load(f) → object

json.dumps and json.loads

import json

order = {"student": "Riya", "items": ("Samosa", "Chai"), "paid": True, "coupon": None}
text = json.dumps(order)
print(text)
print(type(text))

back = json.loads(text)
print(back["items"], back["paid"], back["coupon"])
print(json.dumps({"total": 25}, indent=2))

The tuple came back as a list: JSON has only arrays. JSON keys are always strings.

Real apps you use are API calls underneath: a UPI app asks the bank's API, IRCTC's app asks for seat availability, weather apps ask the IMD. The Government's data.gov.in has free public APIs (prices, rainfall, schools) that you can practise with on a laptop. Most real APIs also need a key sent in headers=; keep keys out of your code and out of git.

In one line

requests.get/post(url, params=/json=, timeout=) returns a response: check status_code or raise_for_status(), then .json(); always set a timeout.

Interview corner

What is the difference between GET and POST?

GET asks for data and should not change anything on the server; its inputs go in the URL as query parameters (params=). POST sends data in the request body to create or change something (json=), and usually answers 201 Created. GET requests can be cached and bookmarked; POST requests should not be repeated blindly.

Does `requests.get` raise an exception for a 404 or 500?

No. Any HTTP answer, even 404 or 500, comes back as a Response. It raises only for network-level failures like Timeout or ConnectionError. Call r.raise_for_status() (or check r.ok) to turn 4xx/5xx into an HTTPError.

Why should you always pass `timeout=` to requests?

By default requests waits indefinitely for a response, so one stuck server can hang your program or a web worker forever. A timeout raises requests.Timeout after the limit, so you can retry, fall back, or show an error. Production code often adds retries with backoff on top.

What is the difference between `json.load` and `json.loads`?

json.loads(text) parses a JSON string; json.load(f) reads from a file object. Likewise json.dumps(obj) returns a string and json.dump(obj, f) writes to a file. With requests, r.json() is json.loads(r.text).