| name | bottle |
| description | [Applies to: **/*.py] Definitive guidelines for building maintainable, secure, and performant Bottle applications in 2025, emphasizing modern Python best practices. |
| source | cursor_mdc |
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
from bottle import route, run, request, Bottle
app = Bottle()
@app.route('/users', method='GET')
def get_users():
return {'users': []}
if __name__ == '__main__':
run(app, host='localhost', port=8080)
✅ GOOD: Structured app.py and routes/users.py
import os
from bottle import Bottle, run
from my_bottle_app.routes import users
from my_bottle_app.plugins.db import DatabasePlugin
app = Bottle()
app.config.load_config('my_bottle_app/config/default.ini')
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)))
app.install(DatabasePlugin(app.config['database.url']))
app.mount('/users', users.app)
if __name__ == '__main__':
run(app, host='0.0.0.0', port=8080, debug=app.config['debug'])
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)
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
DB_URL = "sqlite:///./test.db"
DEBUG_MODE = True
def get_db_connection():
pass
✅ GOOD: app.config with environment overrides
[app]
debug = False
log_level = INFO
[database]
url = sqlite:///./default.db
import os
from bottle import Bottle
app = Bottle()
app.config.load_config('my_bottle_app/config/default.ini')
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'])
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
@app.route('/user/add', method='POST')
def add_user():
name = request.forms.get('name')
email = request.forms.get('email')
if not name or not email:
return '{"error": "Missing fields"}'
return '{"status": "success"}'
✅ GOOD: RESTful routes, Pydantic validation, automatic JSON
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str
email: EmailStr
class UserResponse(BaseModel):
id: int
name: str
email: EmailStr
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()})
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()
4. Dependency Management (Plugins)
Use Bottle plugins for managing shared resources like database connections. Avoid global mutable state.
❌ BAD: Global database connection
import sqlite3
DB_CONN = sqlite3.connect(':memory:')
@app.route('/data')
def get_data():
cursor = DB_CONN.cursor()
✅ GOOD: Plugin-based dependency injection
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):
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
from my_bottle_app.plugins.db import DatabasePlugin
app.install(DatabasePlugin(app.config['database.url']))
@app.get('/', db=True)
() -> [UserResponse]:
cursor = request.db.cursor()
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
def process_data(data, user_id):
return {"status": "ok"}
✅ GOOD: Fully typed functions
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
@app.route('/item/<id>')
def get_item(id):
if not item_exists(id):
raise Exception("Item not found")
✅ GOOD: Custom error handlers, HTTPResponse
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):
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.'})
from bottle import HTTPResponse
def get_resource(resource_id: int):
if not resource_exists(resource_id):
HTTPResponse(status=, body={: })
7. Security Best Practices
Validate all input, sanitize output, and use HTTPS. Implement robust authentication/authorization.
❌ BAD: Direct input use, no output sanitization
@app.get('/search')
def search():
query = request.query.get('q')
return f"<h1>Search results for {query}</h1>"
✅ GOOD: Pydantic validation, html_escape
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
q: str = Field(min_length=1, max_length=100)
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
results = ["Item A", "Item B"]
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
@app.post('/report')
def generate_report():
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
import threading
import time
def _generate_report_async(data: Dict[str, Any]):
time.sleep(5)
print(f"Report generated for {data['user_id']}")
class ReportService:
def generate_report_in_background(self, user_id: int):
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."}
from bottle import Bottle, request
from my_bottle_app.services.report_service import ReportService
app = Bottle()
report_service = ReportService()
@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)
result
9. Testing
Write unit and integration tests using pytest and webtest.
❌ BAD: No automated tests
✅ GOOD: pytest and webtest for route testing
import pytest
from webtest import TestApp
from my_bottle_app.app import app as main_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)
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():
invalid_data = {: }
resp = test_app.post_json(, params=invalid_data, expect_errors=)
resp.status_code ==
resp.json