bottle
Definitive guidelines for building maintainable, secure, and performant Bottle applications in 2025, emphasizing modern Python best practices.
The skill
# bottle Best Practices
Bottle is a robust micro-framework for Python backends. To leverage it effectively in 2025, adhere to these opinionated guidelines, focusing on structure, maintainability, and security.
## 1. Code Organization and Structure
Organize your Bottle project as a Python package with a clear separation of concerns. Avoid monolithic `app.py` files.
**✅ GOOD: Modular Project Structure**
```
my_bottle_app/
├── app.py # Application entry point, config loading, plugin setup
├── routes/ # API endpoint definitions
│ ├── __init__.py
│ └── users.py
│ └── products.py
├── services/ # Business logic, database interactions
│ ├── __init__.py
│ └── user_service.py
│ └── product_service.py
├── schemas/ # Pydantic models for request/response validation
│ ├── __init__.py
│ └── user_schema.py
├── config/ # Configuration files (e.g., default.ini)
│ └── default.ini
├── tests/ # Unit and integration tests
│ └── test_users.py
└── requirements.txt
```
**❌ BAD: Monolithic `app.py`**
```python
# app.py (❌ BAD)
from bottle import route, run, request, Bottle
app = Bottle()
# All routes, logic, and config in one file
@app.route('/users', method='GET')
def get_users():
# ... database logic, business logic, everything here ...
return {'users': []}
# ... many more routes and intertwined logic ...
if __name__ == '__main__':
run(app, host='localhost', port=8080)
```
**✅ GOOD: Structured `app.py` and `routes/users.py`**
```python
# my_bottle_app/app.py (✅ GOOD)
import os
from bottle import Bottle, run
from my_bottle_app.routes import users
from my_bottle_app.plugins.db import DatabasePlugin # Example plugin
app = Bottle()
# 1. Load default config
app.config.load_config('my_bottle_app/config/default.ini')
# 2. Override with environment variables
app.config['database.url'] = os.environ.get('DATABASE_URL', app.config.get('database.url'))
app.config['debug'] = bool(os.environ.get('DEBUG', app.config.get('debug', False)))
# 3. Install plugins
app.install(DatabasePlugin(app.config['database.url']))
# 4. Mount route modules
app.mount('/users', users.app) # users.app is a Bottle instance from routes/users.py
if __name__ == '__main__':
run(app, host='0.0.0.0', port=8080, debug=app.config['debug'])
```
```python
# my_bottle_app/routes/users.py (✅ GOOD)
from bottle import Bottle, request, response, HTTPResponse
from typing import List
from pydantic import ValidationError
from my_bottle_app.schemas.user_schema import UserCreate, UserResponse
from my_bottle_app.services.user_service import UserService
app = Bottle()
@app.get('/')
def get_all_users() -> List[UserResponse]:
"""Retrieves all users."""
user_service = UserService(request.db) # Dependency injection via plugin
users = user_service.get_all()
return [UserResponse.model_validate(u).model_dump() for u in users]
@app.post('/')
def create_user() -> UserResponse:
"""Creates a new user."""
try:
user_data = UserCreate.model_validate(request.json)
except ValidationError as e:
raise HTTPResponse(status=400, body={'error': e.errors()})
user_service = UserService(request.db)
new_user = user_service.create(user_data)
response.status = 201
return UserResponse.model_validate(new_user).model_dump()
```
## 2. Configuration Management
Centralize all application settings in `app.config`. Load defaults from `.ini` files, then override with environment variables. Keep `app.config` immutable after initial load.
**❌ BAD: Hardcoded values and mutable globals**
```python
# ❌ BAD
DB_URL = "sqlite:///./test.db" # Hardcoded
DEBUG_MODE = True
def get_db_connection():
# ... uses DB_URL ...
pass
```
**✅ GOOD: `app.config` with environment overrides**
```python
# my_bottle_app/config/default.ini
[app]
debug = False
log_level = INFO
[database]
url = sqlite:///./default.db
# my_bottle_app/app.py (snippet)
import os
from bottle import Bottle
app = Bottle()
app.config.load_config('my_bottle_app/config/default.ini')
# Override with environment variables
app.config['app.debug'] = os.environ.get('DEBUG', app.config['app.debug']) == 'True'
app.config['database.url'] = os.environ.get('DATABASE_URL', app.config['database.url'])
# Access config values
is_debug = app.config['app.debug']
db_url = app.config['database.url']
```
## 3. API Design and Routing
Design RESTful APIs. Use Pydantic for request/response validation. Return JSON consistently.
**❌ BAD: Inconsistent routes, no validation, manual JSON**
```python
# ❌ BAD
@app.route('/user/add', method='POST') # Not RESTful
def add_user():
name = request.forms.get('name') # No validation
email = request.forms.get('email')
if not name or not email:
return '{"error": "Missing fields"}' # Manual JSON string
# ... logic ...
return '{"status": "success"}'
```
**✅ GOOD: RESTful routes, Pydantic validation, automatic JSON**
```python
# my_bottle_app/schemas/user_schema.py (✅ GOOD)
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str
email: EmailStr
class UserResponse(BaseModel):
id: int
name: str
email: EmailStr
```
```python
# my_bottle_app/routes/users.py (snippet) (✅ GOOD)
from bottle import Bottle, request, response, HTTPResponse
from pydantic import ValidationError
from my_bottle_app.schemas.user_schema import UserCreate, UserResponse
app = Bottle()
@app.post('/')
def create_user() -> UserResponse:
try:
user_data = UserCreate.model_validate(request.json)
except ValidationError as e:
raise HTTPResponse(status=400, body={'error': e.errors()}) # Structured error
# ... call service with user_data.model_dump() ...
new_user_dict = {'id': 1, 'name': user_data.name, 'email': user_data.email}
response.status = 201
return UserResponse.model_validate(new_user_dict).model_dump() # Pydantic handles serialization
```
## 4. Dependency Management (Plugins)
Use Bottle plugins for managing shared resources like database connections. Avoid global mutable state.
**❌ BAD: Global database connection**
```python
# ❌ BAD
import sqlite3
DB_CONN = sqlite3.connect(':memory:') # Global mutable state
@app.route('/data')
def get_data():
cursor = DB_CONN.cursor() # Uses global
# ...
```
**✅ GOOD: Plugin-based dependency injection**
```python
# my_bottle_app/plugins/db.py (✅ GOOD)
import sqlite3
from bottle import Plugin, request, abort
class DatabasePlugin(Plugin):
name = 'database'
api = 2
def __init__(self, db_url: str):
self.db_url = db_url
def apply(self, callback, route):
# Only apply to routes that need a DB
if 'db' not in route.config:
return callback
def wrapper(*args, **kwargs):
try:
request.db = sqlite3.connect(self.db_url)
result = callback(*args, **kwargs)
request.db.commit()
except sqlite3.Error:
request.db.rollback()
abort(500, "Database error")
finally:
request.db.close()
return result
return wrapper
# my_bottle_app/app.py (snippet)
from my_bottle_app.plugins.db import DatabasePlugin
app.install(DatabasePlugin(app.config['database.url']))
# my_bottle_app/routes/users.py (snippet)
@app.get('/', db=True) # Mark route to use the plugin
def get_all_users() -> List[UserResponse]:
cursor = request.db.cursor() # Access via request context
# ...
```
## 5. Type Hints
Apply type hints consistently across all functions, methods, and variables. This improves readability, enables static analysis, and enhances IDE support.
**❌ BAD: Untyped functions**
```python
# ❌ BAD
def process_data(data, user_id):
# ... no types, hard to understand inputs/outputs ...
return {"status": "ok"}
```
**✅ GOOD: Fully typed functions**
```python
# my_bottle_app/services/user_service.py (✅ GOOD)
import sqlite3
from typing import Dict, Any, List
from my_bottle_app.schemas.user_schema import UserCreate, UserResponse
class UserService:
def __init__(self, db_conn: sqlite3.Connection):
self.db_conn = db_conn
def get_all(self) -> List[Dict[str, Any]]:
cursor = self.db_conn.cursor()
cursor.execute("SELECT id, name, email FROM users")
return [dict(zip(['id', 'name', 'email'], row)) for row in cursor.fetchall()]
def create(self, user_data: UserCreate) -> Dict[str, Any]:
cursor = self.db_conn.cursor()
cursor.execute("INSERT INTO users (name, email) VALUES (?, ?)",
(user_data.name, user_data.email))
self.db_conn.commit()
return {'id': cursor.lastrowid, 'name': user_data.name, 'email': user_data.email}
```
## 6. Error Handling
Implement custom error pages for common HTTP errors (400, 404, 500). Raise `HTTPResponse` for controlled error scenarios. Use structured logging for unhandled exceptions.
**❌ BAD: Generic exceptions, no custom error pages**
```python
# ❌ BAD
@app.route('/item/<id>')
def get_item(id):
if not item_exists(id):
raise Exception("Item not found") # Generic exception, 500 status
# ...
```
**✅ GOOD: Custom error handlers, `HTTPResponse`**
```python
# my_bottle_app/app.py (snippet) (✅ GOOD)
import json
from bottle import Bottle, HTTPResponse, request
app = Bottle()
@app.error(404)
def error404(error):
response.content_type = 'application/json'
return json.dumps({'error': 'Not Found', 'message': error.body})
@app.error(400)
def error400(error):
response.content_type = 'application/json'
return json.dumps({'error': 'Bad Request', 'message': error.body})
@app.error(500)
def error500(error):
# Log the full traceback for debugging
app.logger.exception(f"Unhandled error: {error.exception}")
response.content_type = 'application/json'
return json.dumps({'error': 'Internal Server Error', 'message': 'An unexpected error occurred.'})
# In a route handler:
from bottle import HTTPResponse
def get_resource(resource_id: int):
if not resource_exists(resource_id):
raise HTTPResponse(status=404, body={'error': 'Resource not found'}) # Controlled 404
# ...
```
## 7. Security Best Practices
Validate all input, sanitize output, and use HTTPS. Implement robust authentication/authorization.
**❌ BAD: Direct input use, no output sanitization**
```python
# ❌ BAD
@app.get('/search')
def search():
query = request.query.get('q') # Direct use of user input
# ... database query with 'query' (SQL injection risk) ...
return f"<h1>Search results for {query}</h1>" # XSS risk
```
**✅ GOOD: Pydantic validation, `html_escape`**
```python
# my_bottle_app/schemas/search_schema.py (✅ GOOD)
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
q: str = Field(min_length=1, max_length=100)
# my_bottle_app/routes/search.py (✅ GOOD)
from bottle import Bottle, request, HTTPResponse, html_escape
from pydantic import ValidationError
from my_bottle_app.schemas.search_schema import SearchQuery
app = Bottle()
@app.get('/search')
def search_items():
try:
search_params = SearchQuery.model_validate(request.query)
except ValidationError as e:
raise HTTPResponse(status=400, body={'error': e.errors()})
sanitized_query = search_params.q # Input is now validated
# ... safe database query using parameters ...
results = ["Item A", "Item B"] # Example results
# Sanitize output before rendering
escaped_query = html_escape(sanitized_query)
return f"<h1>Search results for {escaped_query}</h1><p>Found: {', '.join(html_escape(r) for r in results)}</p>"
```
## 8. Performance Considerations
Keep route handlers lean. Delegate complex business logic and I/O operations to service layers or background tasks.
**❌ BAD: Heavy computation in a route handler**
```python
# ❌ BAD
@app.post('/report')
def generate_report():
# This will block the web server for a long time
large_data = fetch_huge_dataset()
processed_data = perform_complex_analytics(large_data)
save_report_to_db(processed_data)
return {'status': 'Report generated'}
```
**✅ GOOD: Delegate to a service or background task**
```python
# my_bottle_app/services/report_service.py (✅ GOOD)
import threading
import time
def _generate_report_async(data: Dict[str, Any]):
# Simulate heavy work
time.sleep(5)
print(f"Report generated for {data['user_id']}")
class ReportService:
def generate_report_in_background(self, user_id: int):
# Use a thread or a proper task queue (Celery, RQ) for production
report_data = {'user_id': user_id, 'timestamp': time.time()}
thread = threading.Thread(target=_generate_report_async, args=(report_data,))
thread.start()
return {"message": "Report generation started in background."}
# my_bottle_app/routes/reports.py (✅ GOOD)
from bottle import Bottle, request
from my_bottle_app.services.report_service import ReportService
app = Bottle()
report_service = ReportService() # Instantiate service
@app.post('/reports')
def create_report():
user_id = request.json.get('user_id')
if not user_id:
raise HTTPResponse(status=400, body={'error': 'user_id is required'})
result = report_service.generate_report_in_background(user_id)
return result
```
## 9. Testing
Write unit and integration tests using `pytest` and `webtest`.
**❌ BAD: No automated tests**
```python
# ❌ BAD
# No test files, relying on manual browser checks.
```
**✅ GOOD: `pytest` and `webtest` for route testing**
```python
# my_bottle_app/tests/test_users.py (✅ GOOD)
import pytest
from webtest import TestApp
from my_bottle_app.app import app as main_app # Import your main Bottle app
@pytest.fixture
def test_app():
"""Provides a WebTest client for the Bottle application."""
return TestApp(main_app)
def test_get_all_users(test_app):
"""Test that the /users endpoint returns a list."""
resp = test_app.get('/users')
assert resp.status_code == 200
assert isinstance(resp.json, list)
# Add more specific assertions for content if needed
def test_create_user_success(test_app):
"""Test creating a user successfully."""
user_data = {"name": "Test User", "email": "test@example.com"}
resp = test_app.post_json('/users', params=user_data)
assert resp.status_code == 201
assert resp.json['name'] == "Test User"
assert resp.json['email'] == "test@example.com"
assert 'id' in resp.json
def test_create_user_invalid_data(test_app):
"""Test creating a user with invalid data."""
invalid_data = {"name": "Invalid"} # Missing email
resp = test_app.post_json('/users', params=invalid_data, expect_errors=True)
assert resp.status_code == 400
assert 'error' in resp.json
```
How to install it
1. Open the tool and start a new conversation, project or template. 2. Paste the text below wherever the tool keeps standing instructions — usually called system prompt, instructions, or persona. 3. Save it so it applies to every run rather than a single message.