Skip to content

Python

Python handles QUERY out of the box — http.server dispatches any method via do_<METHOD>, httpx and requests accept arbitrary verbs, and web frameworks (FastAPI, Flask) route by string. No monkey-patching, no hacks.


Server: pure http.server (zero dependencies)

Section titled “Server: pure http.server (zero dependencies)”

http.server calls do_QUERY() if the method exists on the handler. Works on any Python 3.8+, nothing to install.

"""HTTP QUERY server — stdlib only."""
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
CONTACTS = [
{"name": "Ana", "city": "São Paulo", "role": "backend"},
{"name": "Bruno", "city": "Curitiba", "role": "frontend"},
{"name": "Carla", "city": "São Paulo", "role": "devops"},
{"name": "Daniel", "city": "Recife", "role": "backend"},
]
class QueryHandler(BaseHTTPRequestHandler):
def do_QUERY(self):
# Validate Content-Type (RFC 10008 §2.1)
content_type = self.headers.get("Content-Type", "")
if "application/json" not in content_type:
self._respond(415, {"error": "Content-Type must be application/json"})
return
# Read body
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length)) if length else {}
# Filter data
filters = body.get("filter", {})
results = [
c for c in CONTACTS
if all(c.get(k) == v for k, v in filters.items())
]
# Apply limit
limit = body.get("limit", len(results))
results = results[:limit]
self._respond(200, results)
def _respond(self, status, data):
body = json.dumps(data, ensure_ascii=False).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
# RFC 10008 §3 — advertise accepted query formats
self.send_header("Accept-Query", "application/json")
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
server = HTTPServer(("localhost", 8000), QueryHandler)
print("QUERY server running at http://localhost:8000")
server.serve_forever()

BaseHTTPRequestHandler uses dynamic dispatch: any do_XYZ method responds to the XYZ HTTP verb. No registration needed — Python resolves it via introspection. This has actually worked since Python 2.

Tip: Add a do_OPTIONS returning Allow: GET, QUERY, OPTIONS for full spec compliance.


httpx accepts any method via client.request(). It’s Python’s modern HTTP client — async, HTTP/2, typed.

"""QUERY client with httpx."""
import httpx
query = {
"filter": {"city": "São Paulo"},
"limit": 10,
}
with httpx.Client() as client:
resp = client.request(
"QUERY",
"http://localhost:8000/contacts",
json=query,
)
print(resp.status_code) # 200
print(resp.json()) # [{"name": "Ana", ...}, {"name": "Carla", ...}]
import httpx
import asyncio
async def search_contacts():
async with httpx.AsyncClient() as client:
resp = await client.request(
"QUERY",
"http://localhost:8000/contacts",
json={"filter": {"role": "backend"}, "limit": 5},
)
return resp.json()
results = asyncio.run(search_contacts())

httpx has shortcuts for GET, POST, PUT, DELETE — but not QUERY (yet). The request() method is the generic entry point: first argument is the verb string. No restrictions.


FastAPI doesn’t have a @app.query() decorator, but add_api_route() accepts any method in the methods list. Straightforward:

"""FastAPI with QUERY route."""
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
PRODUCTS = [
{"id": 1, "name": "Keyboard", "category": "peripherals", "price": 89},
{"id": 2, "name": "Monitor", "category": "displays", "price": 599},
{"id": 3, "name": "Mouse", "category": "peripherals", "price": 49},
{"id": 4, "name": "SSD", "category": "storage", "price": 129},
]
async def search_products(request: Request):
body = await request.json()
filters = body.get("filter", {})
limit = body.get("limit", 50)
results = [
p for p in PRODUCTS
if all(p.get(k) == v for k, v in filters.items())
]
return JSONResponse(
content=results[:limit],
headers={"Accept-Query": "application/json"},
)
app.add_api_route(
"/products",
search_products,
methods=["QUERY"],
)
Terminal window
uvicorn server:app --reload

FastAPI only generates decorators for standard HTTP methods (@app.get, @app.post, etc.). For custom methods, add_api_route() is the official path — it accepts any string in methods. Starlette underneath doesn’t validate the verb.

Note: The generated OpenAPI schema may not include the QUERY route in Swagger UI until the OpenAPI spec formally recognizes the method.


Flask accepts any string in the methods list of @app.route(). That simple:

"""Flask with QUERY route."""
from flask import Flask, request, jsonify
app = Flask(__name__)
USERS = [
{"id": 1, "name": "Ana", "role": "admin", "active": True},
{"id": 2, "name": "Bruno", "role": "editor", "active": True},
{"id": 3, "name": "Carla", "role": "admin", "active": False},
{"id": 4, "name": "Daniel","role": "viewer", "active": True},
]
@app.route("/users", methods=["QUERY"])
def search_users():
body = request.get_json()
filters = body.get("filter", {})
limit = body.get("limit", 100)
results = [
u for u in USERS
if all(u.get(k) == v for k, v in filters.items())
]
resp = jsonify(results[:limit])
resp.headers["Accept-Query"] = "application/json"
return resp
Terminal window
flask --app server run

Werkzeug (Flask’s WSGI layer) doesn’t restrict HTTP methods — any string is routed normally. Flask just needs it declared in the methods list. No extensions, no hacks.


The requests library accepts arbitrary methods via requests.request():

"""QUERY client with requests."""
import requests
resp = requests.request(
"QUERY",
"http://localhost:8000/contacts",
json={"filter": {"city": "Recife"}, "limit": 5},
)
print(resp.status_code) # 200
print(resp.json()) # [{"name": "Daniel", ...}]
import requests
session = requests.Session()
session.headers.update({
"Accept": "application/json",
})
resp1 = session.request("QUERY", "http://localhost:8000/contacts",
json={"filter": {"role": "backend"}})
resp2 = session.request("QUERY", "http://localhost:8000/contacts",
json={"filter": {"city": "Curitiba"}})

requests doesn’t have a QUERY helper — and probably won’t anytime soon. requests.request() is the generic API that takes any verb. Works the same as httpx.Client().request().


curl accepts any method via -X. Works with all the servers above:

Terminal window
curl -X QUERY http://localhost:8000/contacts \
-H "Content-Type: application/json" \
-d '{"filter": {"city": "São Paulo"}, "limit": 10}'
Terminal window
curl -s -X QUERY http://localhost:8000/contacts \
-H "Content-Type: application/json" \
-d '{"filter": {"role": "backend"}}' | jq .
Terminal window
curl -i -X QUERY http://localhost:8000/contacts \
-H "Content-Type: application/json" \
-d '{"filter": {}}'
Terminal window
curl -i -X QUERY http://localhost:8000/contacts \
-H "Content-Type: text/plain" \
-d 'city=São Paulo'
Flag What it does
-X QUERY Sets the HTTP method
-H "Content-Type: ..." Required header (RFC 10008 §2.1)
-d '{...}' Request body
-i Shows response headers
-s Silences progress bar (useful with jq)
-v Verbose mode — shows the full request on the wire

Tool How to use QUERY
http.server do_QUERY(self) on the handler
httpx client.request("QUERY", url, json=...)
FastAPI app.add_api_route(path, fn, methods=["QUERY"])
Flask @app.route(path, methods=["QUERY"])
requests requests.request("QUERY", url, json=...)
curl curl -X QUERY url -H "Content-Type: ..." -d '...'

Every Python library already supports QUERY — because none of them restrict which HTTP verb you can use. RFC 10008 just standardized what was technically possible all along.