The Other Side of the API
Python programs are not only servers — they call APIs too: fetching data, talking to services, integrating third parties. The standard library's urllib.request is three habits:
1. make the request (urlopen for a simple GET, a Request object for full control over method, headers and body),
2. open the response in a with block so it is always closed,
3. remember that a 4xx or 5xx answer raises HTTPError — catch it where a bad status is a case you handle.
The demos below call a local test server, so the outputs are stable — the client code is exactly what you would point at a real URL. (The ecosystem's popular requests package wraps these same ideas in a friendlier API; everything here stays standard library.)
GET and Decode
The everyday task: GET a URL and decode the JSON body. json.loads(resp.read()) gives plain Python data, and a dataclass turns each dict into a typed record with User(**item).
Fetch a list into records
The response body parses into a list of User records, and from there it is ordinary Python data — looped and printed field by field.
Output: complete Python program
import json
import threading
import urllib.request
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@dataclass
class User:
id: int
name: str
class ListHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'[{"id": 1, "name": "Ana"}, {"id": 2, "name": "Ben"}]')
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), ListHandler)
base = f"http://127.0.0.1:{server.server_address[1]}"
threading.Thread(target=server.serve_forever, daemon=True).start()
with urllib.request.urlopen(base + "/users") as resp:
users = [User(**item) for item in json.loads(resp.read())]
for user in users:
print(f"user {user.id}: {user.name}")
server.shutdown()
POST with a JSON Body
Sending data is the mirror image: dump the record to JSON bytes, POST them with the right content type — then parse the server's answer, which here carries the id the server assigned.
Send a record, read the answer
The record goes out as JSON and the created User(id=7, name='Cleo') comes back — the complete write-path of an API client in one small program.
Output: complete Python program
import json
import threading
import urllib.request
from dataclasses import dataclass, asdict
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@dataclass
class User:
id: int
name: str
class CreateHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
data = json.loads(self.rfile.read(length))
data["id"] = 7 # the server assigns the id
self.send_response(201)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write((json.dumps(data) + "\n").encode())
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), CreateHandler)
base = f"http://127.0.0.1:{server.server_address[1]}"
threading.Thread(target=server.serve_forever, daemon=True).start()
payload = json.dumps(asdict(User(id=0, name="Cleo"))).encode()
req = urllib.request.Request(base + "/users", data=payload,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
created = User(**json.loads(resp.read()))
print("status: ", resp.status, resp.reason)
print("created:", created)
server.shutdown()