Skip to main content

Basics

Essential patterns for Tornado web framework development. Copy-paste ready code examples for app setup, handlers, and routing.

Installation & Setup

Install Tornado

# Install latest version
pip install tornado

# Install specific version
pip install tornado==6.4

# Install with conda
conda install tornado

# Install development version
pip install git+https://github.com/tornadoweb/tornado.git

Basic Project Structure

myapp/
├── app.py # Main application
├── handlers/ # Request handlers
│ ├── __init__.py
│ ├── main.py
│ └── api.py
├── templates/ # HTML templates
│ ├── base.html
│ └── index.html
├── static/ # Static files
│ ├── css/
│ ├── js/
│ └── img/
├── config.py # Configuration
└── requirements.txt # Dependencies

Basic Application Setup

Minimal Application

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, Tornado!")

def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])

if __name__ == "__main__":
app = make_app()
app.listen(8888)
print("Server started on http://localhost:8888")
tornado.ioloop.IOLoop.current().start()

Application with Configuration

import tornado.ioloop
import tornado.web
import tornado.options
from tornado.options import define, options

# Define command line options
define("port", default=8888, help="run on the given port", type=int)
define("debug", default=False, help="run in debug mode")

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, Tornado!")

def make_app():
return tornado.web.Application([
(r"/", MainHandler),
], debug=options.debug)

if __name__ == "__main__":
tornado.options.parse_command_line()
app = make_app()
app.listen(options.port)
print(f"Server started on http://localhost:{options.port}")
tornado.ioloop.IOLoop.current().start()

Application with Settings

import tornado.ioloop
import tornado.web
import os

class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, Tornado!")

def make_app():
settings = {
"debug": True,
"static_path": os.path.join(os.path.dirname(__file__), "static"),
"template_path": os.path.join(os.path.dirname(__file__), "templates"),
"cookie_secret": "your-secret-key-here",
"xsrf_cookies": True,
"login_url": "/login",
"autoreload": True,
"compiled_template_cache": False,
"static_hash_cache": False,
"serve_traceback": True,
}

return tornado.web.Application([
(r"/", MainHandler),
], **settings)

if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()

Request Handlers

Basic Handler Class

import tornado.web

class BaseHandler(tornado.web.RequestHandler):
def get(self):
self.write("GET request")

def post(self):
self.write("POST request")

def put(self):
self.write("PUT request")

def delete(self):
self.write("DELETE request")

def head(self):
# HEAD requests don't return body
pass

def options(self):
self.write("OPTIONS request")

Handler with Arguments

class UserHandler(tornado.web.RequestHandler):
def get(self, user_id):
self.write(f"User ID: {user_id}")

def post(self, user_id):
# Handle POST for specific user
data = self.get_body_argument("data", default="")
self.write(f"Updated user {user_id} with data: {data}")

Handler with Query Parameters

class SearchHandler(tornado.web.RequestHandler):
def get(self):
query = self.get_argument("q", default="")
page = self.get_argument("page", default="1")
limit = self.get_argument("limit", default="10")

self.write({
"query": query,
"page": int(page),
"limit": int(limit)
})

Handler with Form Data

class FormHandler(tornado.web.RequestHandler):
def get(self):
self.render("form.html")

def post(self):
name = self.get_body_argument("name")
email = self.get_body_argument("email")
message = self.get_body_argument("message", default="")

# Process form data
self.write({
"status": "success",
"data": {
"name": name,
"email": email,
"message": message
}
})

Handler with JSON Data

import tornado.escape
import json

class APIHandler(tornado.web.RequestHandler):
def set_default_headers(self):
self.set_header("Content-Type", "application/json")

def get(self):
data = {"message": "Hello, API!"}
self.write(json.dumps(data))

def post(self):
try:
json_data = tornado.escape.json_decode(self.request.body)
# Process JSON data
response = {
"status": "success",
"received": json_data
}
self.write(json.dumps(response))
except ValueError:
self.set_status(400)
self.write(json.dumps({"error": "Invalid JSON"}))

URL Routing

Basic URL Patterns

import tornado.web

app = tornado.web.Application([
# Static URLs
(r"/", MainHandler),
(r"/about", AboutHandler),
(r"/contact", ContactHandler),

# URLs with parameters
(r"/user/([0-9]+)", UserHandler),
(r"/post/([^/]+)", PostHandler),

# Optional parameters
(r"/search/?([^/]*)", SearchHandler),

# Multiple parameters
(r"/category/([^/]+)/post/([0-9]+)", CategoryPostHandler),
])

Named Groups in URLs

import tornado.web

class UserProfileHandler(tornado.web.RequestHandler):
def get(self, user_id):
self.write(f"User Profile: {user_id}")

class PostHandler(tornado.web.RequestHandler):
def get(self, category, post_id):
self.write(f"Category: {category}, Post ID: {post_id}")

app = tornado.web.Application([
(r"/user/(?P<user_id>[0-9]+)", UserProfileHandler),
(r"/(?P<category>[^/]+)/post/(?P<post_id>[0-9]+)", PostHandler),
])

URL Routing with Regex

app = tornado.web.Application([
# Match specific extensions
(r"/files/(.+\.(jpg|jpeg|png|gif))", ImageHandler),

# Match API versions
(r"/api/v([1-9])/users", APIUserHandler),

# Match optional trailing slash
(r"/page/?", PageHandler),

# Match slugs (letters, numbers, hyphens)
(r"/blog/([a-zA-Z0-9-]+)", BlogPostHandler),
])

Static File Serving

import tornado.web
import os

settings = {
"static_path": os.path.join(os.path.dirname(__file__), "static"),
"static_url_prefix": "/static/",
}

app = tornado.web.Application([
(r"/", MainHandler),

# Serve static files manually
(r"/static/(.*)", tornado.web.StaticFileHandler,
{"path": settings["static_path"]}),

# Serve uploaded files
(r"/uploads/(.*)", tornado.web.StaticFileHandler,
{"path": "/path/to/uploads"}),

# Serve with custom headers
(r"/downloads/(.*)", tornado.web.StaticFileHandler,
{"path": "/path/to/downloads", "default_filename": "index.html"}),
], **settings)

Application Configuration

Environment-based Configuration

import os
import tornado.web

class Config:
DEBUG = os.environ.get("DEBUG", "False").lower() == "true"
PORT = int(os.environ.get("PORT", "8888"))
SECRET_KEY = os.environ.get("SECRET_KEY", "your-secret-key")
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///app.db")

def make_app():
settings = {
"debug": Config.DEBUG,
"cookie_secret": Config.SECRET_KEY,
"xsrf_cookies": True,
"static_path": os.path.join(os.path.dirname(__file__), "static"),
"template_path": os.path.join(os.path.dirname(__file__), "templates"),
}

return tornado.web.Application([
(r"/", MainHandler),
], **settings)

if __name__ == "__main__":
app = make_app()
app.listen(Config.PORT)
print(f"Server running on port {Config.PORT}")
tornado.ioloop.IOLoop.current().start()

Configuration with YAML

import yaml
import tornado.web

def load_config(config_file="config.yaml"):
with open(config_file, 'r') as f:
return yaml.safe_load(f)

def make_app():
config = load_config()

settings = {
"debug": config.get("debug", False),
"cookie_secret": config.get("secret_key"),
"static_path": config.get("static_path", "static"),
"template_path": config.get("template_path", "templates"),
}

return tornado.web.Application([
(r"/", MainHandler),
], **settings)

Configuration Class

import tornado.web
import os

class AppConfig:
def __init__(self):
self.debug = os.environ.get("DEBUG", "False").lower() == "true"
self.port = int(os.environ.get("PORT", "8888"))
self.secret_key = os.environ.get("SECRET_KEY", "dev-secret-key")
self.static_path = os.path.join(os.path.dirname(__file__), "static")
self.template_path = os.path.join(os.path.dirname(__file__), "templates")
self.database_url = os.environ.get("DATABASE_URL", "sqlite:///app.db")

def to_dict(self):
return {
"debug": self.debug,
"cookie_secret": self.secret_key,
"static_path": self.static_path,
"template_path": self.template_path,
"xsrf_cookies": True,
"login_url": "/login",
}

def make_app():
config = AppConfig()
return tornado.web.Application([
(r"/", MainHandler),
], **config.to_dict())

Application Lifecycle

Startup and Shutdown

import tornado.ioloop
import tornado.web
import signal
import logging

class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r"/", MainHandler),
]
settings = {"debug": True}
super().__init__(handlers, **settings)

# Initialize application
self.setup_logging()
self.connect_database()

def setup_logging(self):
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

def connect_database(self):
# Initialize database connection
pass

def cleanup(self):
# Cleanup resources
logging.info("Shutting down application")

def signal_handler(signum, frame):
logging.info(f"Received signal {signum}")
tornado.ioloop.IOLoop.current().stop()

if __name__ == "__main__":
# Setup signal handlers
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

app = Application()
app.listen(8888)

logging.info("Starting server on port 8888")
try:
tornado.ioloop.IOLoop.current().start()
except KeyboardInterrupt:
pass
finally:
app.cleanup()

Application Factory Pattern

import tornado.web
import tornado.ioloop
from handlers import MainHandler, UserHandler, APIHandler

def create_app(debug=False):
"""Application factory function"""
handlers = [
(r"/", MainHandler),
(r"/user/([0-9]+)", UserHandler),
(r"/api/.*", APIHandler),
]

settings = {
"debug": debug,
"cookie_secret": "your-secret-key",
"xsrf_cookies": True,
"static_path": "static",
"template_path": "templates",
}

app = tornado.web.Application(handlers, **settings)
return app

def main():
app = create_app(debug=True)
app.listen(8888)
print("Server started on http://localhost:8888")
tornado.ioloop.IOLoop.current().start()

if __name__ == "__main__":
main()

Error Handling

Custom Error Handler

import tornado.web
import logging

class BaseHandler(tornado.web.RequestHandler):
def write_error(self, status_code, **kwargs):
if status_code == 404:
self.render("404.html")
elif status_code == 500:
self.render("500.html")
else:
self.write(f"Error {status_code}")

def log_exception(self, typ, value, tb):
logging.error(f"Exception: {typ.__name__}: {value}", exc_info=(typ, value, tb))

class MainHandler(BaseHandler):
def get(self):
# This will trigger a 404 if not found
self.write("Hello, World!")

Global Error Handler

import tornado.web
import tornado.ioloop
import logging

class ErrorHandler(tornado.web.RequestHandler):
def prepare(self):
self.set_status(404)
self.render("404.html")

def make_app():
settings = {
"debug": False,
"default_handler_class": ErrorHandler,
"template_path": "templates",
}

return tornado.web.Application([
(r"/", MainHandler),
], **settings)

if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()

Development Tools

Debug Mode

import tornado.web
import tornado.autoreload
import tornado.ioloop

def make_app():
settings = {
"debug": True, # Enable debug mode
"autoreload": True, # Auto-reload on file changes
"compiled_template_cache": False, # Disable template caching
"static_hash_cache": False, # Disable static file caching
"serve_traceback": True, # Show tracebacks in browser
}

app = tornado.web.Application([
(r"/", MainHandler),
], **settings)

return app

if __name__ == "__main__":
app = make_app()
app.listen(8888)

# Watch additional files for changes
tornado.autoreload.watch("config.py")
tornado.autoreload.watch("handlers/")

tornado.ioloop.IOLoop.current().start()

Logging Configuration

import logging
import tornado.web
import tornado.log

# Configure logging
logging.getLogger().setLevel(logging.INFO)
tornado.log.enable_pretty_logging()

# Custom logger
logger = logging.getLogger(__name__)

class MainHandler(tornado.web.RequestHandler):
def get(self):
logger.info("Handling GET request")
self.write("Hello, World!")

def post(self):
logger.info("Handling POST request")
self.write("POST received")

This covers the fundamental patterns for getting started with Tornado. The framework's power comes from its asynchronous capabilities and WebSocket support, which are covered in the advanced sections.