CherryPy Python Framework Guide — Build Lightweight Web Applications
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
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 frameworkclass HelloWorld— a normal Python class (not a special base class)@cherrypy.expose— tells CherryPy this method is accessible via HTTPindex(self)— the default page (maps to/)greet(self, name="Guest")— maps to/greet?name=Aliceor/greet/Alicecherrypy.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 argumentThis 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
indexmethod, it is called without arguments. - Positional parameters — Extra path segments become positional arguments:
/greet/Alicecallsgreet(self, "Alice"). - Query parameters — Values from
?name=Aliceare available viacherrypy.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
| Scenario | Recommended Framework |
|---|---|
| Lightweight microservice or internal API | CherryPy or Flask |
| Full-featured web app with user auth | Django |
| High-performance async API with auto docs | FastAPI |
| Real-time WebSocket application | CherryPy (built-in) or FastAPI |
| Admin dashboard with database | CherryPy + 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
| Feature | Description |
|---|---|
| Object-oriented | URL structure maps directly to Python object hierarchy |
| Built-in server | Multi-threaded production-ready HTTP server included |
| Tools system | Authentication, caching, encoding, compression built in |
| Static hosting | Built-in static file serving |
| WebSocket | Support for WebSocket connections |
| Templating | Integration with Jinja2, Mako, and others |
| Testing | Built-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):
pass3. 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
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.
| Topic | Description | Link |
|---|---|---|
| Flask Framework | Lightweight Python web framework | https://tutorials.dodatech.com/programming-languages/python/flask/reference/ |
| FastAPI Framework | Modern async Python framework | https://tutorials.dodatech.com/programming-languages/python/fastapi/reference/ |
| Django Framework | Full-stack Python web framework | https://tutorials.dodatech.com/programming-languages/python/django/reference/ |
| Python Basics | Review core Python concepts | https://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