One Handler per Route
Routing decides which code answers which request. The standard http.server has no router built in — and none is needed for the everyday cases: do_GET looks at self.path and dispatches.
A path parameter is the piece of the URL that is data: in /users/7, the 7. Splitting the path into segments hands it to you: "/users/7".strip("/").split("/") gives ['users', '7']. Frameworks add pattern routers on top, but this is what they do underneath.
Routes and Path Values
Two routes: the list at /users and one user at /users/7. The id segment is the path parameter — the part of the URL that carries data.
A route with a path parameter
The same do_GET sends /users to one branch and /users/7 to another — one split, and the second segment delivers the 7.
Output: complete Python program
import threading
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class UsersHandler(BaseHTTPRequestHandler):
def do_GET(self):
parts = self.path.strip("/").split("/") # "/users/7" -> ["users", "7"]
if parts == ["users"]:
self.reply("all users: Ana, Ben, Cleo\n")
elif len(parts) == 2 and parts[0] == "users":
user_id = parts[1] # the path parameter
self.reply(f"one user, id = {user_id}\n")
else:
self.reply("nothing here\n", status=404)
def reply(self, text, status=200):
self.send_response(status)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(text.encode())
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), UsersHandler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
for path in ["/users", "/users/7"]:
with urllib.request.urlopen(f"http://127.0.0.1:{port}{path}") as resp:
print("GET", path, "->", resp.read().decode(), end="")
server.shutdown()
Query Parameters
The part after ? holds query parameters — options for the request, like a search text or a limit: /search?q=python&limit=2. urlparse splits them off the path and parse_qs turns them into a dict of lists — always strings, with .get covering the missing ones.
The rule of thumb: the path says which resource, the query says how — filters, sorting, paging.
Reading the query string
The handler reads q and limit from the query. The second request skips limit, so the handler falls back to a default — missing parameters are normal, plan for them.
Output: complete Python program
import threading
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
class SearchHandler(BaseHTTPRequestHandler):
def do_GET(self):
url = urlparse(self.path) # splits path and query
params = parse_qs(url.query) # {"q": ["python"], ...}
q = params.get("q", [""])[0]
limit = params.get("limit", ["10"])[0] # default for missing ones
body = f"searching for {q} with limit {limit}\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(body.encode())
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), SearchHandler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
for path in ["/search?q=python&limit=2", "/search?q=json"]:
with urllib.request.urlopen(f"http://127.0.0.1:{port}{path}") as resp:
print("GET", path, "->", resp.read().decode(), end="")
server.shutdown()