Skip to content
CherryPy Python Framework Guide — Build Lightweight Web Applications

CherryPy Python Framework Guide — Build Lightweight Web Applications

DodaTech Updated Jun 6, 2026 9 min read

CherryPy is a minimalist Python web framework that lets you build web applications using familiar object-oriented patterns, with a production-ready HTTP server built right in.

What You’ll Learn

  • How CherryPy maps Python objects to web URLs
  • How to set up routes, handle requests, and return responses
  • How to use CherryPy’s built-in tools for common tasks
  • How to serve static files and integrate templates
  • How to deploy a CherryPy application

Why CherryPy Matters

Not every web application needs the overhead of a full-stack framework. CherryPy’s minimalist approach makes it ideal for microservices, internal tools, and lightweight APIs. Doda Browser uses CherryPy for its internal configuration server. Durga Antivirus Pro leverages CherryPy for lightweight REST endpoints that check signature updates. Its built-in production server means you don’t need a separate WSGI server like Gunicorn.

    flowchart LR
    A["Python Basics"] --> B["CherryPy"]
    B --> C["Web Apps"]
    B --> D["REST APIs"]
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#2563eb,stroke:#2563eb,color:#fff
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
  
Prerequisite: You should understand Python basics including functions and modules. Familiarity with HTTP concepts (GET, POST, status codes) is helpful.

Core Concepts

CherryPy’s philosophy is simple: URLs map to Python objects. Each public method decorated with @cherrypy.expose becomes a web page:

import cherrypy

class HelloWorld:
    @cherrypy.expose
    def index(self):
        return "<h1>Hello World!</h1>"

    @cherrypy.expose
    def greet(self, name="Guest"):
        return f"<h1>Hello, {name}!</h1>"

if __name__ == "__main__":
    cherrypy.quickstart(HelloWorld())

Let’s break this down:

  • import cherrypy — import the framework
  • class HelloWorld — a normal Python class (not a special base class)
  • @cherrypy.expose — tells CherryPy this method is accessible via HTTP
  • index(self) — the default page (maps to /)
  • greet(self, name="Guest") — maps to /greet?name=Alice or /greet/Alice
  • cherrypy.quickstart() — starts the server

Run this file and visit http://localhost:8080. You’ll see “Hello World!”. Visit http://localhost:8080/greet?name=Alice to see the greet method.

URL Dispatching

CherryPy’s URL mapping follows rules that feel natural to Python developers:

class Root:
    @cherrypy.expose
    def index(self):
        return "Home page"

    @cherrypy.expose
    def about(self):
        return "About page"

class Blog:
    @cherrypy.expose
    def index(self):
        return "Blog home"

    @cherrypy.expose
    def post(self, id):
        return f"Blog post {id}"

# Mount controllers at different paths
root = Root()
root.blog = Blog()

cherrypy.quickstart(root)

Now / maps to Root.index, /about to Root.about, /blog/ to Blog.index, and /blog/post?id=5 to Blog.post.

Request and Response

CherryPy provides access to request and response objects:

import cherrypy

class Echo:
    @cherrypy.expose
    def index(self):
        # Access query parameters
        name = cherrypy.request.params.get("name", "World")
        # Set response headers
        cherrypy.response.headers["X-Custom"] = "hello"
        return f"Hello, {name}!"

    @cherrypy.expose
    def json(self):
        # Return JSON
        cherrypy.response.headers["Content-Type"] = "application/json"
        return '{"message": "Hello, JSON!"}'

    @cherrypy.expose
    def upload(self):
        # Handle file uploads
        if cherrypy.request.method == "POST":
            data = cherrypy.request.body.read()
            return f"Received {len(data)} bytes"
        return """
        <form method="post" enctype="multipart/form-data">
            <input type="file" name="file">
            <button type="submit">Upload</button>
        </form>
        """

Static Files and Templates

CherryPy can serve static files and integrate with template engines:

import cherrypy
import os

class App:
    @cherrypy.expose
    def index(self):
        return open("static/index.html").read()

# Static file configuration
config = {
    "/static": {
        "tools.staticdir.on": True,
        "tools.staticdir.dir": os.path.join(os.getcwd(), "static"),
    },
    "/": {
        "tools.sessions.on": True,  # Enable sessions
    }
}

cherrypy.quickstart(App(), "/", config)

Built-in Tools

CherryPy includes tools for common tasks:

import cherrypy
from cherrypy import tools

class SecureAPI:
    @cherrypy.expose
    @tools.json_in()       # Parse JSON request body
    @tools.json_out()      # Return JSON response
    def data(self):
        input_data = cherrypy.request.json  # Parsed JSON
        return {"received": input_data}

    @cherrypy.expose
    @tools.auth_basic(realm="Private")
    def secret(self, username, password):
        return f"Hello, {username}!"

    @cherrypy.expose
    @tools.caching()
    def cached_page(self):
        return "This response is cached"

How CherryPy’s Dispatcher Works

CherryPy’s URL dispatching is built on Python’s attribute access model. When a request arrives, CherryPy parses the URL path into segments, then traverses the mounted object tree by accessing each segment as a Python attribute:

Request: GET /blog/post/42

1. Start at root object (mounted at /)
2. Access root.blog → Blog instance
3. Access root.blog.post → bound method
4. Call root.blog.post(42) with remaining path segment as argument

This is fundamentally different from frameworks that use route decorators or configuration files. The URL structure is a direct reflection of your Python object hierarchy, making the code predictable and easy to reason about.

CherryPy’s dispatcher supports several parameter passing modes:

  • Default method — If the final URL segment matches an index method, it is called without arguments.
  • Positional parameters — Extra path segments become positional arguments: /greet/Alice calls greet(self, "Alice").
  • Query parameters — Values from ?name=Alice are available via cherrypy.request.params["name"] and also match method keyword arguments.
  • Trailing slash behavior — A trailing slash tells CherryPy the next segment is an attribute name; no trailing slash means the last segment is the method to call.

Database Integration with SQLAlchemy

CherryPy integrates naturally with SQLAlchemy for persistent storage. Here is a complete note-taking API backed by SQLite:

import cherrypy
from sqlalchemy import create_engine, Column, Integer, String, Text
from sqlalchemy.orm import declarative_base, sessionmaker

Base = declarative_base()
engine = create_engine("sqlite:///notes.db")
Session = sessionmaker(bind=engine)

class Note(Base):
    __tablename__ = "notes"
    id = Column(Integer, primary_key=True)
    title = Column(String(100))
    content = Column(Text)

Base.metadata.create_all(engine)

class NotesAPI:
    @cherrypy.expose
    @cherrypy.tools.json_out()
    def index(self):
        session = Session()
        notes = session.query(Note).all()
        result = [{"id": n.id, "title": n.title, "content": n.content} for n in notes]
        session.close()
        return result

    @cherrypy.expose
    @cherrypy.tools.json_in()
    @cherrypy.tools.json_out()
    def create(self):
        data = cherrypy.request.json
        session = Session()
        note = Note(title=data["title"], content=data.get("content", ""))
        session.add(note)
        session.commit()
        note_id = note.id
        session.close()
        return {"id": note_id, "message": "Note created"}

    @cherrypy.expose
    @cherrypy.tools.json_out()
    def get(self, note_id):
        session = Session()
        note = session.query(Note).filter_by(id=note_id).first()
        session.close()
        if not note:
            raise cherrypy.HTTPError(404, "Note not found")
        return {"id": note.id, "title": note.title, "content": note.content}

if __name__ == "__main__":
    cherrypy.quickstart(NotesAPI())

Expected output: GET / returns [{"id": 1, "title": "Demo", "content": "Hello"}]. POST /create with {"title": "New Note", "content": "Content"} returns {"id": 2, "message": "Note created"}. GET /get/1 returns the single note. The SQLite database persists data across server restarts, so your notes survive a reboot.

WebSocket Support

CherryPy includes built-in WebSocket support through its cherrypy.websocket plugin:

import cherrypy
from cherrypy import expose
from cherrypy.websocket import WebSocketPlugin

class ChatWebSocket:
    @expose
    def index(self):
        return """
        <!DOCTYPE html>
        <html>
        <body>
        <script>
        const ws = new WebSocket("ws://" + location.host + "/ws");
        ws.onmessage = function(e) {
            document.body.innerHTML += "<p>" + e.data + "</p>";
        };
        ws.onopen = function() {
            ws.send("Hello server!");
        };
        </script>
        </body>
        </html>
        """

    @expose
    def ws(self):
        cherrypy.request.ws_handler = self

    def recv(self, msg):
        print("Received:", msg)
        cherrypy.request.ws_handler.send("Echo: " + msg)

if __name__ == "__main__":
    WebSocketPlugin(cherrypy.engine).subscribe()
    cherrypy.quickstart(ChatWebSocket())

Expected output: Opening the page in a browser establishes a WebSocket connection to ws://localhost:8080/ws. The client sends “Hello server!” and receives “Echo: Hello server!” back. This pattern is ideal for live dashboards like the Durga Antivirus Pro threat monitoring interface.

CherryPy vs Other Frameworks

ScenarioRecommended Framework
Lightweight microservice or internal APICherryPy or Flask
Full-featured web app with user authDjango
High-performance async API with auto docsFastAPI
Real-time WebSocket applicationCherryPy (built-in) or FastAPI
Admin dashboard with databaseCherryPy + SQLAlchemy

CherryPy excels when you want a production-ready server without configuring Gunicorn or uWSGI, prefer object-oriented design, or need to expose existing Python classes as web endpoints with minimal changes.

Key Features

FeatureDescription
Object-orientedURL structure maps directly to Python object hierarchy
Built-in serverMulti-threaded production-ready HTTP server included
Tools systemAuthentication, caching, encoding, compression built in
Static hostingBuilt-in static file serving
WebSocketSupport for WebSocket connections
TemplatingIntegration with Jinja2, Mako, and others
TestingBuilt-in test helper for unit testing

Common Mistakes

1. Forgetting @cherrypy.expose

Without @cherrypy.expose, the method won’t be accessible via HTTP — you’ll get a 404.

2. Not Specifying Request Method

CherryPy methods respond to all HTTP methods by default. Restrict with:

@cherrypy.expose
@tools.accept(media="application/json")
def api(self):
    pass

3. Returning Non-String Responses

CherryPy expects string responses. For other types, convert them:

return str(my_list)

4. Not Configuring Static Files

Static files won’t be served unless you configure tools.staticdir. Always set tools.staticdir.dir to an absolute path.

Practice Questions

1. What does @cherrypy.expose do?

It makes a Python method accessible as a web URL. Without it, the method can’t be called via HTTP.

2. How does CherryPy map URLs to code?

URLs map to Python object attributes. /blog/post maps to root.blog.post().

3. Does CherryPy need a separate web server like Gunicorn?

No. CherryPy includes its own production-ready HTTP server. Just run cherrypy.quickstart().

Challenge: Build a simple REST API with CherryPy that stores notes in memory. Create endpoints for listing notes (GET /notes), adding a note (POST /notes), and getting a single note (GET /notes/{id}).

4. How does CherryPy integrate with SQLAlchemy?

Create SQLAlchemy model classes, set up an engine and session factory, then use sessions inside exposed methods to query and persist data. CherryPy does not impose any specific ORM — you can use SQLAlchemy, Peewee, or raw database drivers.

5. What are the different ways to pass parameters to a CherryPy handler?

Parameters can come from URL path segments (positional), query string parameters, or a JSON request body (with @tools.json_in()). CherryPy matches method parameter names against all these sources automatically.

Advanced Challenge: Build a full-featured REST API with CherryPy and SQLAlchemy for managing a task list. Include GET /tasks (list all), POST /tasks (create with title and due_date, return 201), GET /tasks/{id} (single task), PUT /tasks/{id} (update status between pending/done), and DELETE /tasks/{id} (delete). Add @tools.auth_basic checking credentials against a dictionary. Return proper HTTP status codes: 400 for bad input, 404 for missing resources, 401 for auth failure.

Mini Project — Signature Update Server: Build a lightweight CherryPy server for Durga Antivirus Pro that serves virus signature updates. Expose GET /latest-version returning the current version number, GET /download/{version} serving a file with correct Content-Type and Content-Disposition headers. Require basic authentication for the download endpoint. Log every request with timestamp and client IP. Keep the code under 200 lines — CherryPy’s minimal design makes this achievable.

FAQ

How is CherryPy different from Flask?
CherryPy is more minimalist and object-oriented. It includes its own production server. Flask is more popular and has a larger ecosystem, but requires a separate WSGI server for production.
Is CherryPy suitable for production?
Yes. CherryPy’s built-in server is multi-threaded and production-ready. Many companies use it in production for microservices and APIs.
Can CherryPy handle WebSockets?
Yes. CherryPy has built-in WebSocket support through its cherrypy.websocket plugin.

Try It Yourself

Save this as app.py and run it:

import cherrypy

class HelloWorld:
    @cherrypy.expose
    def index(self):
        return "<h1>Hello World!</h1>"

    @cherrypy.expose
    def greet(self, name="Guest"):
        return f"<h1>Hello, {name}!</h1>"

if __name__ == "__main__":
    cherrypy.quickstart(HelloWorld())

Run python app.py and visit http://localhost:8080. You should see “Hello World!”.

What’s Next

Now that you’ve learned CherryPy, explore other Python frameworks.

TopicDescriptionLink
Flask FrameworkLightweight Python web frameworkhttps://tutorials.dodatech.com/programming-languages/python/flask/reference/
FastAPI FrameworkModern async Python frameworkhttps://tutorials.dodatech.com/programming-languages/python/fastapi/reference/
Django FrameworkFull-stack Python web frameworkhttps://tutorials.dodatech.com/programming-languages/python/django/reference/
Python BasicsReview core Python conceptshttps://tutorials.dodatech.com/programming-languages/python/py-basics/

What’s Next

Congratulations on completing this Cherrypy tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro