Bottle: Complete Guide to the Python Microframework
Introduction to Bottle
Bottle is a minimalist Python microframework for building web applications and REST APIs. The entire framework is written in a single file under 100 KB, making it an ideal solution for small projects, prototypes, embedded systems, and micro‑services. Bottle provides all essential components to run a full‑featured web application without external dependencies.
Features and Advantages of Bottle
Lightweight and Simple
Bottle requires no external dependencies and can be installed with a single command. The framework consists of just one file, which simplifies deployment and code comprehension.
Built‑in Capabilities
- Built‑in development HTTP server
- SimpleTemplate Engine templating system
- WSGI support for production deployment
- Automatic route parameter type detection
- Handling of JSON, forms, file uploads, and cookies
Architectural Flexibility
Bottle works with any Python libraries for databases, templating engines, and WSGI servers, allowing easy integration into existing systems.
Installation and Basic Setup
Installing Bottle
pip install bottle
Creating Your First Application
Create a file app.py:
from bottle import route, run
@route('/')
def index():
return "Hello from Bottle!"
@route('/hello/<name>')
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
run(host='localhost', port=8080, debug=True)
Running the Application
python app.py
The application will be accessible at http://localhost:8080
Routing and Request Handling
Basic Routes
from bottle import route, get, post, put, delete
@route('/')
def home():
return "Home page"
@get('/users')
def get_users():
return "User list"
@post('/users')
def create_user():
return "Create user"
@put('/users/<id:int>')
def update_user(id):
return f"Update user {id}"
@delete('/users/<id:int>')
def delete_user(id):
return f"Delete user {id}"
Route Parameters
Bottle supports various parameter types:
@route('/user/<id:int>')
def user_by_id(id):
return f"User ID: {id}"
@route('/article/<slug:path>')
def article_by_slug(slug):
return f"Article: {slug}"
@route('/price/<amount:float>')
def price(amount):
return f"Price: {amount}"
@route('/profile/<username:re:[a-zA-Z0-9_]+>')
def profile(username):
return f"Profile: {username}"
Query Parameter Handling
from bottle import request
@route('/search')
def search():
query = request.query.get('q', '')
category = request.query.get('category', 'all')
return f"Search '{query}' in category '{category}'"
Working with Request Data
Form Processing
from bottle import request, post
@post('/submit')
def submit_form():
name = request.forms.get('name')
email = request.forms.get('email')
age = request.forms.get('age', type=int)
return f"Received data: {name}, {email}, {age}"
JSON Handling
from bottle import request, response
@post('/api/data')
def handle_json():
data = request.json
# Process data
result = {
'status': 'success',
'received': data,
'message': 'Data processed successfully'
}
response.content_type = 'application/json'
return result
File Uploads
import os
from bottle import request, post
@post('/upload')
def upload_file():
upload = request.files.get('file')
if upload:
# Validate file extension
name, ext = os.path.splitext(upload.filename)
if ext not in ['.jpg', '.png', '.gif']:
return "Unsupported file format"
# Save file
upload.save('./uploads/')
return f"File {upload.filename} uploaded successfully"
return "No file selected"
Templating and Data Rendering
Built‑in Templating Engine
Create a file templates/user.tpl:
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<h1>Profile {{name}}</h1>
<p>Email: {{email}}</p>
<p>Age: {{age}}</p>
% if posts:
<h2>Recent Posts:</h2>
<ul>
% for post in posts:
<li>{{post['title']}}</li>
% end
</ul>
% else:
<p>No posts</p>
% end
</body>
</html>
Usage in code:
from bottle import template
@route('/user/<id:int>')
def user_profile(id):
user_data = {
'name': 'John Doe',
'email': 'john@example.com',
'age': 30,
'posts': [
{'title': 'First post'},
{'title': 'Second post'}
]
}
return template('user', **user_data)
Integration with Jinja2
from bottle import route, run, install
from bottle import jinja2_plugin
# Install Jinja2
install(jinja2_plugin)
@route('/hello/<name>')
def hello(name):
return {'name': name}
Cookies and Sessions
Cookie Management
from bottle import response, request
@route('/login')
def login():
response.set_cookie('user_id', '123', max_age=3600)
response.set_cookie('session', 'abc123', httponly=True, secure=True)
return "Logged in"
@route('/profile')
def profile():
user_id = request.get_cookie('user_id')
if user_id:
return f"Welcome, user {user_id}"
else:
return "Please log in"
@route('/logout')
def logout():
response.delete_cookie('user_id')
response.delete_cookie('session')
return "Logged out"
Simple Sessions
import uuid
from bottle import request, response
# Simple in‑memory session store
sessions = {}
def get_session():
session_id = request.get_cookie('session_id')
if session_id and session_id in sessions:
return sessions[session_id]
return None
def create_session(data):
session_id = str(uuid.uuid4())
sessions[session_id] = data
response.set_cookie('session_id', session_id)
return session_id
@route('/login', method='POST')
def login():
username = request.forms.get('username')
password = request.forms.get('password')
# Credential check
if username == 'admin' and password == 'secret':
create_session({'username': username, 'logged_in': True})
return "Logged in"
else:
return "Invalid credentials"
Error and Exception Handling
Standard Error Handlers
from bottle import error, abort
@error(404)
def error404(error):
return "Page not found. Check the URL."
@error(500)
def error500(error):
return "Internal server error. Please try again later."
@route('/protected')
def protected():
user = request.get_cookie('user')
if not user:
abort(401, "Access denied")
return "Protected page"
Custom Exceptions
class ValidationError(Exception):
pass
@route('/api/validate', method='POST')
def validate_data():
try:
data = request.json
if not data or 'email' not in data:
raise ValidationError("Email is required")
# Validate email format
if '@' not in data['email']:
raise ValidationError("Invalid email address")
return {"status": "success", "message": "Data is valid"}
except ValidationError as e:
response.status = 400
return {"status": "error", "message": str(e)}
Database Integration
Connecting SQLite
import sqlite3
from bottle import g
def get_db():
if not hasattr(g, 'db'):
g.db = sqlite3.connect('app.db')
g.db.row_factory = sqlite3.Row
return g.db
@route('/users')
def get_users():
db = get_db()
cursor = db.cursor()
cursor.execute('SELECT * FROM users')
users = [dict(row) for row in cursor.fetchall()]
return {'users': users}
@post('/users')
def create_user():
db = get_db()
cursor = db.cursor()
name = request.forms.get('name')
email = request.forms.get('email')
cursor.execute(
'INSERT INTO users (name, email) VALUES (?, ?)',
(name, email)
)
db.commit()
return {'status': 'success', 'message': 'User created'}
Using an ORM (SQLAlchemy)
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
engine = create_engine('sqlite:///app.db')
Session = sessionmaker(bind=engine)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
email = Column(String(100))
Base.metadata.create_all(engine)
@route('/users')
def get_users():
session = Session()
users = session.query(User).all()
result = [{'id': u.id, 'name': u.name, 'email': u.email} for u in users]
session.close()
return {'users': result}
Static Files and Media
Serving Static Files
from bottle import static_file
@route('/static/<filename:path>')
def static_files(filename):
return static_file(filename, root='./static')
@route('/download/<filename>')
def download_file(filename):
return static_file(filename, root='./downloads', download=filename)
@route('/image/<filename:re:.*\.(jpg|png|gif)>')
def serve_image(filename):
return static_file(filename, root='./images')
Project Structure
project/
├── app.py
├── templates/
│ ├── base.tpl
│ ├── index.tpl
│ └── user.tpl
├── static/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── app.js
│ └── images/
│ └── logo.png
├── uploads/
├── downloads/
└── database.db
REST API with Bottle
Building a Full‑Featured API
from bottle import route, get, post, put, delete, request, response
import json
# In‑memory data store
users = [
{'id': 1, 'name': 'Ivan', 'email': 'ivan@example.com'},
{'id': 2, 'name': 'Peter', 'email': 'peter@example.com'}
]
def json_response(data):
response.content_type = 'application/json'
return json.dumps(data, ensure_ascii=False)
@get('/api/users')
def get_all_users():
return json_response({'users': users})
@get('/api/users/<id:int>')
def get_user(id):
user = next((u for u in users if u['id'] == id), None)
if user:
return json_response(user)
else:
response.status = 404
return json_response({'error': 'User not found'})
@post('/api/users')
def create_user():
data = request.json
new_id = max(u['id'] for u in users) + 1 if users else 1
new_user = {
'id': new_id,
'name': data.get('name'),
'email': data.get('email')
}
users.append(new_user)
response.status = 201
return json_response(new_user)
@put('/api/users/<id:int>')
def update_user(id):
user = next((u for u in users if u['id'] == id), None)
if not user:
response.status = 404
return json_response({'error': 'User not found'})
data = request.json
user.update({
'name': data.get('name', user['name']),
'email': data.get('email', user['email'])
})
return json_response(user)
@delete('/api/users/<id:int>')
def delete_user(id):
global users
users = [u for u in users if u['id'] != id]
response.status = 204
return ""
Middleware and Plugins
Creating Middleware
from functools import wraps
from bottle import request, response, abort
def cors_middleware():
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
def auth_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization')
if not token or not token.startswith('Bearer '):
abort(401, "Access token missing")
# Validate token
if not is_valid_token(token[7:]):
abort(401, "Invalid token")
return f(*args, **kwargs)
return decorated_function
def is_valid_token(token):
# Simple token check
return token == "valid_token_123"
@route('/api/protected')
@auth_required
def protected_endpoint():
cors_middleware()
return {'message': 'Access granted'}
Request Logging
import logging
from bottle import hook
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@hook('before_request')
def log_request():
logger.info(f"{request.method} {request.path} - {request.environ.get('REMOTE_ADDR')}")
@hook('after_request')
def log_response():
logger.info(f"Response: {response.status}")
Deploying Applications
Using Gunicorn
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 app:app
Docker Configuration
Dockerfile:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]
requirements.txt:
bottle
gunicorn
Nginx Configuration
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Key Bottle Methods and Functions
| Method/Function | Description | Example Usage |
|---|---|---|
@route(path, method='GET') |
Decorator for defining routes | @route('/users/<id:int>') |
@get(path) |
Decorator for GET requests | @get('/users') |
@post(path) |
Decorator for POST requests | @post('/users') |
@put(path) |
Decorator for PUT requests | @put('/users/<id:int>') |
@delete(path) |
Decorator for DELETE requests | @delete('/users/<id:int>') |
run(host, port, debug) |
Starts the built‑in server | run(host='localhost', port=8080) |
request.query |
Access query parameters | request.query.get('q') |
request.forms |
Access form data | request.forms.get('name') |
request.json |
Access JSON payload | request.json.get('key') |
request.files |
Access uploaded files | request.files.get('upload') |
request.headers |
Access request headers | request.headers.get('Authorization') |
request.get_cookie(name) |
Retrieve a cookie | request.get_cookie('session_id') |
response.status |
Set response status code | response.status = 404 |
response.content_type |
Set response content type | response.content_type = 'application/json' |
response.set_cookie(name, value) |
Set a cookie | response.set_cookie('user', 'admin') |
response.delete_cookie(name) |
Delete a cookie | response.delete_cookie('session') |
template(name, **kwargs) |
Render a template | template('index', name='user') |
static_file(filename, root) |
Serve static files | static_file('style.css', root='./static') |
redirect(url) |
Redirect to another URL | redirect('/login') |
abort(code, message) |
Abort with an error | abort(401, 'Access denied') |
@error(code) |
Error handler decorator | @error(404) |
@hook('before_request') |
Hook executed before each request | @hook('before_request') |
@hook('after_request') |
Hook executed after each request | @hook('after_request') |
install(plugin) |
Install a plugin | install(cors_plugin) |
Frequently Asked Questions
What is Bottle and what is it used for?
Bottle is a minimalist Python web framework for building REST APIs, small web applications, and prototypes. It is perfect for rapid development and requires no external dependencies.
Can Bottle be used for large projects?
Bottle is best suited for small to medium‑sized projects. For large‑scale applications, more feature‑rich frameworks like Django or FastAPI are recommended.
How can I integrate external templating engines?
Bottle supports integration with popular templating engines such as Jinja2, Mako, and Cheetah via appropriate plugins.
Does Bottle provide authentication out of the box?
Bottle does not include a built‑in authentication system, but you can easily implement one yourself using cookies, sessions, or JWT tokens.
Can Bottle work with databases?
Yes, Bottle is compatible with any Python database library, including SQLite, PostgreSQL, MySQL, and ORMs like SQLAlchemy.
How do I deploy a Bottle application to production?
For production, it is recommended to use WSGI servers such as Gunicorn, uWSGI, or Waitress together with a reverse proxy like Nginx.
Does Bottle support WebSocket?
Bottle does not have native WebSocket support, but you can use specialized plugins or integrate with libraries like eventlet or gevent.
Conclusion
Bottle is an excellent choice for developers who need a simple, fast, and lightweight web framework. Its minimalist architecture and lack of external dependencies make it ideal for prototyping, micro‑service creation, and small‑scale web applications. With solid documentation and an active community, Bottle remains a popular tool for rapid Python web development.
$
The Future of AI in Mathematics and Everyday Life: How Intelligent Agents Are Already Changing the Game
Experts warned about the risks of fake charity with AI
In Russia, universal AI-agent for robots and industrial processes was developed