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
| Python | JSON text |
|---|---|
| dict {"a": 1} | object {"a": 1} |
| list or tuple | array [1, 2] |
| True / False | true / false |
| None | null |
| json.dumps(obj) → text | json.loads(text) → object |
| json.dump(obj, f) → file | json.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).