2025, Sep 30 13:00
CherryPy _cp_dispatch explained: reliable REST URL routing by returning the handler and avoiding vpath side effects
Learn how to use CherryPy _cp_dispatch for clean REST URL routing: return the handler, manage vpath and params, avoid 404 confusion and multiple-value errors.
CherryPy’s _cp_dispatch gives you a low-level hook to bend URL routing to your will. It’s perfect when you need clean REST-style paths that don’t map one-to-one to method names. The catch: if you mutate the path segments blindly and keep returning self, CherryPy may keep traversing as if you had sub-objects, and your handler never gets called. Below is a compact walkthrough of the pitfall and the fix.
Problem setup
The goal is straightforward: map /batch/ to a specific method, and /batch/{id}/product/ or /batch/{id}/product/{sku} to another. Everything else should yield 404. The initial attempt rewrites vpath and returns self, expecting CherryPy to restart resolution against the modified path.
import cherrypy
class GoodsApi(object):
    def __init__(self, data_store):
        self.data_store = data_store
    def _cp_dispatch(self, vpath):
        # /batch/
        if len(vpath) == 1 and vpath[0] == "batch":
            vpath[0] = "batch_entry"
            return self
        # /batch/{id}/product/
        if len(vpath) == 3 and vpath[0] == "batch":
            vpath.pop(0)  # remove "batch"
            cherrypy.request.params["batch_id"] = vpath.pop(0)
            return self
        # /batch/{id}/product/{sku}
        if len(vpath) == 4 and vpath[0] == "batch" and vpath[2] == "product":
            vpath.pop(0)  # remove "batch"
            cherrypy.request.params["batch_id"] = vpath.pop(0)
            vpath.pop(0)  # remove "product"
            cherrypy.request.params["sku"] = vpath[0]
            vpath[0] = "item"
            return self
        # default traversal
        return self
    @cherrypy.expose
    @cherrypy.tools.json_out()
    def batch_entry(self):
        # intended for /batch/
        pass
    @cherrypy.expose
    @cherrypy.tools.json_out()
    def item(self, batch_id, sku=None):
        # intended for /batch/{id}/product[/ {sku}]
        pass
What’s really going on
CherryPy first looks for exposed handlers that match the path. If none are found, it invokes _cp_dispatch(self, vpath) with the remaining segments. Inside this hook you have two valid strategies. You can mutate vpath and return self to continue traversal, or you can return a specific bound method to finish resolution immediately. If the hook returns None, CherryPy responds with 404.
Rewriting a segment like vpath[0] = "batch_entry" doesn’t make CherryPy call the method directly; it keeps treating the result as another traversal step. This is why the /batch/ handler never fires in the initial attempt. An additional subtlety is that leftover segments in vpath can bleed into argument resolution, which explains issues like “multiple values for batch_id.” Fetching the parameters in _cp_dispatch and ensuring there are no stale segments avoids that confusion.
Fix: return the handler directly
The robust approach is to stop rewriting the path and instead return the target handler from _cp_dispatch. Populate cherrypy.request.params as needed, consume the path pieces you used, and let CherryPy call the method you returned. Paths that don’t match should yield None to trigger 404.
import cherrypy
class GoodsApi(object):
    def __init__(self, data_store):
        self.data_store = data_store
    def _cp_dispatch(self, vpath):
        # /batch/
        if len(vpath) == 1 and vpath[0] == "batch":
            return getattr(self, "batch_entry")
        # /batch/{id}/product/
        if len(vpath) == 3 and vpath[0] == "batch":
            vpath.pop(0)  # remove "batch"
            cherrypy.request.params["batch_id"] = vpath.pop(0)
            return getattr(self, "item")
        # /batch/{id}/product/{sku}
        if len(vpath) == 4 and vpath[0] == "batch" and vpath[2] == "product":
            vpath.pop(0)  # remove "batch"
            cherrypy.request.params["batch_id"] = vpath.pop(0)
            vpath.pop(0)  # remove "product"
            cherrypy.request.params["sku"] = vpath.pop(0)
            return getattr(self, "item")
        # anything else → 404
        return None
    @cherrypy.expose
    @cherrypy.tools.json_out()
    def batch_entry(self):
        return {"msg": "batch list or create batch here"}
    @cherrypy.expose
    @cherrypy.tools.json_out()
    def item(self, batch_id, sku=None):
        return {"batch": batch_id, "sku": sku}
if __name__ == "__main__":
    cherrypy.quickstart(GoodsApi({}), "/rest-products")
This wiring produces the intended behavior. Requests to /rest-products/batch/ invoke batch_entry() for both GET and POST. Requests to /rest-products/batch/123/product/ invoke item(batch_id=123). Requests to /rest-products/batch/123/product/ABC123 invoke item(batch_id=123, sku="ABC123"). Any other paths result in 404.
Why this matters
Custom REST routing often lives in the gray area between URL shape and handler signatures. Understanding that _cp_dispatch either continues traversal with a mutated vpath or short-circuits by returning a handler helps avoid ambiguous resolution. It also prevents subtle parameter collisions, such as getting multiple values for the same argument when both vpath leftovers and request.params try to supply it.
Takeaways
Use _cp_dispatch to collect exactly the pieces you need from the URL, store them in cherrypy.request.params, and either return the handler or None. Avoid renaming path segments to simulate method names and returning self, because CherryPy will treat them as regular traversal steps rather than a direct method target. When the goal is clarity and strict 404s for non-matching paths, returning the handler explicitly keeps routing deterministic and easy to reason about.
The article is based on a question from StackOverflow by Bart Friederichs and an answer by Madhav Singh Rana.