ToDo Application Example

This tutorial gives a brief introduction to theBottleWSGI Framework. The main goal this tutorial is, after finishing working yourself through, to be able to create a project using Bottle. Within this document, by far not all features are shown, but at least the main and important ones like request routing, utilizing the Bottle template engine to format output as well as handling GET and POST request are featured. The last section gives a brief introduction how to serve a Bottle application by a WSGI application server.

To understand the content of this tutorial, it is not really necessary to have a basic knowledge of WSGI, as Bottle tries to keep WSGI away from the user anyway as much as possible. A fair bit of understanding of thePython programming language is of course required. Furthermore, the example application created in this tutorial retrieves and stores data in a SQL database, so (very) basic knowledge on SQL helps, but is not a must to understand the concepts of Bottle. Right here,SQLite is used. As Bottle is a framework for web-based application, most of output send to the Browser is HTML. Thus, a basic idea about the common HTML tags certainly helps as well. In case HTML basic still need to be learned, a good starting point is theHTML tutorial on the Mozilla Developer Network website.

For the sake of introducing Bottle, the Python code “in between” is kept short, in order to keep the focus. Although all code within the tutorial works fine, it may not necessarily be used as-is “in the wild”, e.g. on a publically accessible server. To do so, e.g. input validtion, better database protection, better error handling and other things need to be added.

Goals

At the end of this tutorial, a ready-to-use, simple, web-based ToDo application is going to be programmed. The app takes tasks, each one consisting of a text (with max 100 characters) and a status (0 for closed, 1 for open). Through the web-based user interface, open task can be view and edited and new tasks can be added.

During development, all pages will be available under the address127.0.0.1 (aka:localhost) in a web browser running on the same machine as the Bottle application code. Later on it will be shown how to adapt the application for a “real” server.

Bottle will do the routing and format the output with the help of templates. The tasks will be stored inside a SQLite database. Reading and writing the database will be done by Python code.

The result of this tutorial is going to be an application with the following pages and functionality:

  • start pagehttp://127.0.0.1:8080/todo

  • adding a new task to the list:http://127.0.0.1:8080/new

  • page for editing a task:http://127.0.0.1:8080/edit/<number:int>

  • show details about a task:http://127.0.0.1:8080/details/<number:int>

  • show a task formated as JSON:http://127.0.0.1:8080/as_json/<number:int>

  • redirecthttp://127.0.0.1:8080/ tohttp://127.0.0.1:8080/todo

  • catching errors

Prior to Starting …

A Note on Python Versions

Bottle supports a wide range of Python version. Bottle 0.13 supports Python 3.8 and newer as well as Python 2 starting with 2.7.3, although Python 2 support will be dropped with Bottle 0.14. As Python 2 support was dropped by the Python core developers on Jan 1st 2020 already, it is highly encourage to use a recent Python 3 release.

This tutorial requires at least Python 3.10, as at one point thematch statement is going to be used, which was introduced with Python 3.10. In case Python 3.8 or 3.9 is going to be used, the match statement needs to be replaced with an if-elif-else cascade, all other sections will work just fine. If really Python 2.7.x must be used, additionally thef-strings used at some places needs to be replaced with the string formatting methods available in Python 2.7.x.

And, finally,python will be used in this tutorial to run Python 3.10 and newer. On some platforms it may be necessary to typepython3 instead to run the installed Python 3 interpreter.

Install Bottle

Assuming that a fairly new installation of Python (version 3.10 or higher) is used, only Bottle needs to be installed in addition to that. Bottle has no other dependencies than Python itself. Following the recommended best-pratice for Python and installing Python modules, let’s create avenv first and install Bottle inside the venv. Open the directory of choice where the venv should be created and execute the following commands:

python-mvenvbottle_venvcdbottle_venv#for Linux & MacOSsourcebin/activate#for Windows.\Scripts\activatepip3installbottle

SQLite

This tutorial uses _SQLite as the database. The standard release of Python has SQlite already on board and haveSQLite module included to interact with the database. So no further installation is required here.

Create An SQL Database

Prior to starting to work on the ToDo application, the database to be used later on needs to be created. To do so, save the following script in the project directory and run it with python. Alternatively, the lines of code can be executed in the interactive Python interpreter, too:

importsqlite3connection=sqlite3.connect('todo.db')# Warning: This file is created in the current directorycursor=connection.cursor()cursor.execute("CREATE TABLE todo (id INTEGER PRIMARY KEY, task char(100) NOT NULL, status bool NOT NULL)")cursor.execute("INSERT INTO todo (task,status) VALUES ('Read the Python tutorial to get a good introduction into Python',0)")cursor.execute("INSERT INTO todo (task,status) VALUES ('Visit the Python website',1)")cursor.execute("INSERT INTO todo (task,status) VALUES ('Test various editors for and check the syntax highlighting',1)")cursor.execute("INSERT INTO todo (task,status) VALUES ('Choose your favorite WSGI-Framework',0)")connection.commit()

These commands generate a database-file namedtodo.db with a table calledtodo. The table has three columnsid,task, andstatus.id is a unique id for each row, which is used later to reference rows of data. The columntask holds the text which describes the task, it is limited to max 100 characters. Finally, the columnstatus is used to mark a task as open (represented by the value 1) or closed (represented by the value 0).

Writing a Web-Based ToDo Application with Bottle

Let’s dive into Bottle and create the web-based ToDo application. But first, let’s look into a basic concept of Bottle: routes.

Understanding routes

Basically, each page visible in the browser is dynamically generated when the page address is called. Thus, there is no static content. That is exactly what is called a “route” within Bottle: a certain address on the server. So, for example, opening the URLhttp://127.0.0.1:8080/todo from the browser, Bottle “grabs” the call on the server-side and checks if there is any (Python) function defined for the route “todo”. If so, Bottle executes the corresponding Python code and returns its result. So, what Bottle (as well as other Python WSGI frameworks) does: it binds an URL to a function.

Bottle basic by a “Hello World” example

Before finally starting the ToDo app, let’s create a very basic “Hello World” example:

frombottleimportBottleapp=Bottle()@app.route('/')defindex():return'Hello from Bottle'if__name__=='__main__':app.run(host='127.0.0.1',port=8080)

Save the file under a name of choice, e.g.hello_bottle.py and execute the filepythonhello_bottle.py. Then open the browser and enterhttp://127.0.0.1:8080 in the address bar. The browser window should now show the text “Hello from Bottle”.

So, what happens here? Let’s dissect line by line:

  • frombottleimportBottle imports theBottle class from the Bottle module. Each instance derived from the classrepresents a single, distinct web application.

  • app=Bottle() creates an instance ofBottle.app is the web application object.

  • @app.route('/') creates a new route bond to/ for the app.

  • defindex() defines a function which is “linked” to the route/, as theindex function is decorated withtheapp.route decorator (more on that below).

  • return'HellofromBottle' “Hello from Bottle” is the plain text send to the browser when the route is called.

  • if__name__=='__main__':: The following code is only execute when the file holding the code is directly executedby the Python interpreter. In case e.g. a WSGI server is serving the code (more on that later), the following codeis not executed.

  • app.run(host='127.0.0.1',port=8080) starts the build-in development server, listing on the address127.0.0.1and port8080.

First Step - Showing All Open Items

So, after understanding the concept of routes and the basics of Bottle, let’s create the first real route for the ToDo application. The goal is to see all open items from the ToDo list:

importsqlite3frombottleimportBottleapp=Bottle()@app.route('/todo')deftodo_list():withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("SELECT id, task, status FROM todo WHERE status LIKE '1'")result=cursor.fetchall()returnstr(result)if__name__=='__main__':app.run(host='127.0.0.1',port=8080)

Save the code astodo.py, preferably in the same directory as the database filetodo.db. Otherwise, the path totodo.db must be added in thesqlite3.connect() statement.

Let’s have a look what happens here: the required modulesqlite3 is imported to access to SQLite database, and from Bottle theBottle class is imported. One function is defined,todo_list(), with a few lines of code reading from the database. The important point here is thedecorator function@route('/todo') right before thedeftodo_list() statement. By doing this, this function is bound to the route/todo, so every time the browsers callshttp://127.0.0.1:8080/todo, Bottle returns the result of the functiontodo_list(). That is how routing within bottle works.

Actually, more than one route can be bound to a function. The following code:

@route('/todo')@route('/my_todo_list')deftodo_list():...

works fine, too. What will not work is to bind one route to more than one function.

What the browser displays is what is returned, thus the value given by thereturn statement. In this example, it is necessary to convertresult in to a string bystr(), as Bottle expects a string or a list of strings from the return statement. But here, the result of the database query is a list of tuples, which is the standard defined by thePython DB API.

Now, after understanding the little script above, it is time to execute it and watch the result. Just runpythontodo.py and open the URLhttp://127.0.0.1:8080/todo in the browser. In case no mistake was made writing the code, the output should look like this:

[(2,'Visit the Python website',1),(3,'Test various editors for and check the syntax highlighting',1)]

If so - congratulations! Bottle is successful used. In case it did not work, and changes need to be made, remember to stop Bottle serving the page, otherwise the revised version will not be loaded.

The output is not really exciting nor nice to read. It is the raw result returned from the SQL query. In the next step the output is formated in a nicer way. But before that, let’s make life a bit easier while developing the app.

Debugging and Auto-Reloading

Maybe it was already noticed that Bottle sends a short error message to the browser in case something within the script went wrong, e.g. the connection to the database is not working. For debugging purposes, it is quite helpful to get more details. This can be easily achieved by adding the following to the script:

frombottleimportBottle...if__name__=='__main__':app.run(host='127.0.0.1',port=8080,debug=True)

By enabling “debug”, a full stacktrace of the Python interpreter will be received in case of an error, which usually contains useful information, helping to find the error. Furthermore, templates (see below) are not cached, thus changes to templates will take effect without stopping and restarting the server.

Warning

debug=True is supposed to be used for development only, it shouldnot be used in production environments.

Another nice feature while developing is auto-reloading, which is enabled by modifying theapp.run() statement to

app.run(host='127.0.0.1',port=8080,reloader=True)

This will automatically detect changes to the script and reload the new version once it is called again, without the need to stop and start the server.

Again, the feature is mainly supposed to be used while developing, not on production systems.

Bottle’s SimpleTemplate To Format The Output

Now let’s have a look at casting the output of the script into a proper format. Actually, Bottle expects to receive a string or a list of strings from a function and returns them to the browser. Bottle does not bother about the content of the string itself, so it can be e.g. text formatted with HTML markup.

Bottle has its own easy-to-use, build-in template engine called “SimpleTemplate”. Templates are stored as separate files having a.tpl extension. And by default, they are expected to be in a directory calledviews below the directory where the Python code of the application is located. A template can be called from within a function. Templates can contain any type of text (which will be most likely HTML-markup mixed with Python statements). Furthermore, templates can take arguments, e.g. the result set of a database query, which will be then formatted nicely within the template.

Right here, the result of the query showing the open ToDo tasks is cast into a simple HTML table with two columns: the first column will contain the ID of the item, the second column the text. The result is, as seen above, a list of tuples, each tuple contains one set of results.

To include the template in the example, just add the following lines:

frombottleimportBottle,template...result=cursor.fetchall()output=template('show_tasks',rows=result)returnoutput...

Two things are done here: first,template additionally imported from bottle in order to be able to use templates. Second, the output of the templateshow_tasks is assigned to the variableoutput, which then is returned. In addition to calling the template,result is assigned, which is received from the database query, to the variablerows, which passed to the template to be used within the template later on. If necessary, more than one variable / value can be passed to a template.

Templates always return a list of strings, thus there is no need to convert anything. One line of code can be saved by writingreturntemplate('show_tasks',rows=result), which gives exactly the same result as above.

Now it is time to write the corresponding template, which looks like this:

%#template to generate a HTML table from a list of tuples (or list of lists, or tuple of tuples or ...)<p>The open items are as follows:</p><tableborder="1">%for row in rows:<tr>  %for col in row:<td>{{col}}</td>  %end</tr>%end</table>

Save the code asshow_tasks.tpl in theviews directory.

Let’s have a look at the code: every line starting with % is interpreted as Python code. Because it is effectively Python, only valid Python statements are allowed. The template will raise exceptions, just as any other Python code would, in case of wrong code. The other lines are plain HTML markup.

As can be seen, Python’sfor statement is used two times, to go throughrows. As seen above,rows is a variable which holds the result of the database query, so it is a list of tuples. The firstfor statement accesses the tuples within the list, the second one the items within the tuple, which are put each into a cell of the table. It is important that allfor,if,while etc. statements are closed with%end, otherwise the output will not be as expected.

If a variable within a non-Python code line needs to be accessed inside the template, put it into double curly braces, like{{col }} in the example above. This tells the template to insert the actual value of the variable right at this place.

Run the script again and look at the output. Still not really nice and not complete HTML, but at least more readable than the list of tuples.

Adding a Base Template

Bottle’s SimpleTempate allows, like other template engines, nesting templates. This is pretty handy, as it allows to define a base template holding e.g. the HTML doctype definition, the head and the body section, which is then used as the base for all other templates generating the actual output. The base template looks like this:

<!doctype html><htmllang="en-US"><head><metacharset="utf-8"/><title>ToDo App powered by Bottle</title></head><body>{{!base}}</body></html>

Save this template with the namebase.tpl in theviews folder.

As can be seen, the template holds a basic HTML skeleton for a typically website. The{{!base}} inserts the content of the other template using the base template.

To use the base template from another template like e.g.shows_task.tpl, just add the following line at the beginning of this template:

%rebase('base.tpl')...

This tells the template to rebase its content into the templatebase.tpl.

Reloadhttp://127.0.0.1:8000/todo and the output is now valid HTML. Of couse the base template can extended as required, e.g. by loading a CSS style sheet or defining own styles in a<style>...</style> section in the header.

Using GET Parameters

The app has its first route showing task, but so far it only shows the open tasks. Let’s modify this functionality and add an (optional) GET parameter to the route which lets the user choose whether to show open tasks only (which is at the same time the default), only closed tasks or all tasks stored in the database. This should be achieved by checking for a key namedshow, which can have one of the following three values:open,closed orall. So e.g. opening the URLhttp://127.0.0.1:8080?show=all should make the application show all tasks from the database.

The updated route and corresponding function look like this:

...frombottleimportrequest...@app.get('/todo')deftodo_list():show=request.query.showor'open'matchshow:case'open':db_query="SELECT id, task FROM todo WHERE status LIKE '1'"case'closed':db_query="SELECT id, task FROM todo WHERE status LIKE '0'"case'all':db_query="SELECT id, task FROM todo"case_:returntemplate('message.tpl',message='Wrong query parameter: show must be either open, closed or all.')withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute(db_query)result=cursor.fetchall()output=template('show_tasks.tpl',rows=result)returnoutput...

At first,request is added to the imports from Bottle. Therequest object of Bottle holds all data from a request sent to the application. Additionally, the route is change to@app.get(...) to explicitly state that this route only excepts GET requests only.

Note

This change is not strictly necessary, asapp.route() accepts implicitly GET request only, too. However, following theZen of Python : “Explicit is better than implicit.”

The lineshow_all =request.query.showor'open' does the following:query is the attribute of therequest object holding the data from a GET request. Sorequest.query.show returns the value of the keyshow from the request. Ifshow is not present, the valueopen is assigned to theshow variable. This also implies that any other key in the GET request is ignored.

The followingmatch statement assigns a SQL query to the variabledb_query depending on the value ofshow, respectively shows an error message ifshow is neitheropen norclosed norall. The remaining code of thetodo_list() function remains unchanged.

While working on this route, let’s make one addition to theshow_tasks template. Add the line

<p><ahref="/new">Add a new task</a></p>

at the end of the template to add a link for adding a new task to the database. The corresponding route and function will be created in the following section.

And, finally, the new templatemessage.tpl used in the code about, looks like this:

% rebase('base.tpl')<p>{{ message }}</p><p><ahref="/todo">Back to main page</p>

Using Forms and POST Data

As all tasks now can be viewed properly, let’s move to the next step and add the functionality to add a new task to the ToDo list. The new task should be received from a regular HTML form, sending its data by a POST request.

To do so, first a new route is added to the code. The route should accept GET and POST requests:

@app.route('/new',method=['GET','POST'])defnew_task():ifrequest.POST:new_task=request.forms.task.strip()withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("INSERT INTO todo (task,status) VALUES (?,?)",(new_task,1))new_id=cursor.lastrowidreturntemplate('message.tpl',message=f'The new task was inserted into the database, the ID is{new_id}')else:returntemplate('new_task.tpl')

A new route is created, assigned to/new, which accepts GET as well as POST requests. Inside the functionnew_task assigned to this route, therequest object introduced in the previous section is checked to see whether a GET or a POST request was received:

...ifrequest.POST:#The code here is only executed if POST data, e.g. from a#HTML form, is inside the request.else:#the code here is only executed if no POST data was received....

request.forms is the attribute which holds data submitted by an HTML from.request.forms.task holds the data from the fieldtask of the form. Astask is a string, thestrip method is additionally applied to remove any white spaces before or after the string.

Then the new task is written to the database, and the ID of the new task is return. If no POST data was received, the templatenew_task is send. This template holds the HTML form to enter a new task. The template looks like this:

%#template of the form for a new task% rebase('base.tpl')<p>Add a new task to the ToDo list:</p><formaction="/new"method="post"><p><inputtype="text"size="100"maxlength="100"name="task"></p><p><inputtype="submit"name="save"value="save"></p></form>

Editing Existing Items

The last piece missing to complete the simple ToDo app is the functionality to edit existing tasks in the database. Either to change their status or to update the text of a task.

By using only the routes introduced so far it is possible, but will be quite tricky. To make things easier, let’s use Bottle’s feature calleddynamic routes , which makes this coding task quite easy.

The basic statement for a dynamic route looks like this:

..code-block::python

@app.route(‘some_route/<something>’)

<something> is called a “wildcard”. Furthermore, the value of the wildcardsomething is be passed to the function assigned to this route, so the data can be processed within the function. Optionally, a filter can be applied to the wildcard. The filter does one thing: it checks whether the wildcard matches a certain type of data, e.g. an integer value or a regular expression. If not, an error is raised.

Theint filter is used for this route, which checks at first if the wildcard matches an integer value and. If yes, the wildcard string is converted to a Python integer object.

The complete route for editing a task looks like this:

@app.route('/edit/<number:int>',method=['GET','POST'])defedit_task(number):ifrequest.POST:new_data=request.forms.task.strip()status=request.forms.status.strip()ifstatus=='open':status=1else:status=0withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("UPDATE todo SET task = ?, status = ? WHERE id LIKE ?",(new_data,status,number))returntemplate('message.tpl',message=f'The task number{number} was successfully updated')else:withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("SELECT task FROM todo WHERE id LIKE ?",(number,))current_data=cursor.fetchone()returntemplate('edit_task',current_data=current_data,number=number)

A lot of the code’s logic is pretty similat to the/new route and the correspondingnew_task function: the route accepts GET and POST requests and, depending on the request, either sends the templateedit_task or updates a task in the database according to the form data received.

What’s new here is the dynamic routing@app.route('/edit/<number:int>'...) which accepts one wildcard, supposed to be an integer value. The wildcard is assigned to the variablenumber, which is also expected by the functionedit_task. So e.g. opening the URLhttp:/127.0.0.1:8080/edit/2 would open the task with the ID for editing. In case no number is passed, either because of omitting the parameter or passing a string which is not an integer only, an error will be raised.

The templateedit_task.tpl called within the function looks like this:

%#template for editing a task%#the template expects to receive a value for "number" as well a "old", the text of the selected ToDo item% rebase('base.tpl')<p>Edit the task with ID = {{number}}</p><formaction="/edit/{{number}}"method="post"><p><inputtype="text"name="task"value="{{current_data[0]}}"size="100"maxlength="100"><selectname="status"><option>open</option><option>closed</option></select></p><p><inputtype="submit"name="save"value="save"></p></form>

The next section “Returning JSON Data” shows another example of a dynamic route using a filter.

Returning JSON Data

A nice feature of Bottle is that it automatically generates a response with content typeJSON is a Python dictionary is passed to the return statement of a route. Which makes it very easy to build web-based APIs with Bottle. Let’s build a route for the ToDo app application which returns a task from the database as JSON. This is pretty straight forward; the code looks like this:

@app.route('/as_json/<number:re:[0-9]+>')deftask_as_json(number):withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("SELECT id, task, status FROM todo WHERE id LIKE ?",(number,))result=cursor.fetchone()ifnotresult:return{'task':'This task ID number does not exist!'}else:return{'id':result[0],'task':result[1],'status':result[2]}

As can be seen, the only difference is the dictionary returned. Either resulting in a JSON object with the three keys “id”, “task” and “status” or with one key named “task” only, having the error message as the value.

Additionally, there filter applying a RegEx is used for the wildcardnumber of this route. Of course theint filter as used for the/edit` route could be used here, too (and would be probably more appropriate), but the RegEx filter is used just to showcase it here. The filter can basically handle any regular expression Python’sRegEx module can handle.

Returning Static Files

Sometimes it may become necessary to associate a route not to a Python function but just return a static file. A static file could be e.g. a JPG or PNG graphics, a PDF file or a static HTML file instead of a template. In any case, another import needs to be added first

frombottleimportstatic_file

to the code to import Bottle’s functionstatic_file, which handles sending static files. Let’s assume all the static files are located in a subdirectory namedstatic relativ to the application. The code to serve static files from there looks as follows:

...frompathlibimportPathABSOLUTE_APPLICATION_PATH=Path(__file__).parent[0]...@app.route('/static/<filepath:path>')defsend_static_file(filepath):ROOT_PATH=ABSOLUTE_APPLICATION_PATH/'static'returnstatic_file(filepath,root=ROOT_PATH)

ThePath class of Python’spathlib module is imported and then used to determine the absolute path where the application is located. This is necessary, as thestatic_file method requires an absolute path to the static content. Of course, the path could be hard coded into the code, but using pathlib is more elegant.

The route/static/<filepath:path> makes use of Bottle’s build-inpath filter and the wildcard holding the name of the file to be served is assigned to thefilepath. As can be seen from the code, thestatic_file function requires the name of the file to be served as well as the root path to the directory where the file is located.

Bottle guesses the MIME-type of the file automatically. But it can also be stated explicitly by adding a third argument tostatic_file, e.g.mimetype='text/html' for serving a static HTML file. More information onstatic_file can be found in thestatic_file documentation .

Catching Errors

When trying to open a webpage which doesn’t exist, a “404 Not Found” error message is displayed in the browser. Bottle offers an option to catch these errors and return a customized error message instead. This works as follows:

@app.error(404)deferror_404(error):return'Sorry, this page does not exist!'

In the event a 404 Not Found error occurs, the function decorated withapp.error(404) is run and returns the customized error message of choice. Theerror argument passed to the function holds a tuple with two elements: the first element is the actual error code and the second element the actual error message. This tuple can be used within the function but does not have to. Of course, if is also possible, like for all routes, to assign more than one error / route to a function, like e.g.:

@app.error(404)@error(403)defsomething_went_wrong(error):returnf'{error}: There is something wrong!'

Create a Redirect (Bonus Section)

Although the ToDo application works just fine, it still has one little flaw: When trying to open127.0.01:8080 in the browser, the root route, a 404 error will occur, as no route is established for`/. Which is not too much of a problem, but at least a little bit unexpected. Of course this could be changed by modifiying the routeapp.route('/todo') toapp.route('/'). Or, if the /todo route should be kept, a redirect can be added to the code. Again, this is pretty straight forward:

...frombottleimportredirect...@app.route('/')defindex():redirect('/todo')

At first, the (so far) missing routeapp.route('/') is added, decorating theindex() function. It has only one line of code, redirecting the browser to the todo route. When opening the URL127.0.0.1:8080, the browser will be automatically redirect tohttp://127.0.0.1:8080/todo.

Summary

After going through all the sections above, a brief understanding on how Bottle works is hopefully achieved so new Bottle-based web applications can be written.

The following chapter will be show how to serve Bottle with web servers with perform better on a higher load / more web traffic than the one used so far.

Deployment

So far, the built-in development server of Bottle was used, which based on theWSGI reference Server included in Python. Although this server is perfectly fine and very handy for development purposes, it is not really suitable to serve “real world” applications. But before looking at the alternatives, let’s have a look how to tweak the settings of the build-in server first.

Running Bottle on a different port and IP

As a standard, Bottle serves on the IP address 127.0.0.1, also known aslocalhost, and on port8080. To modify the setting is pretty simple, as additional parameters can be passed to Bottle’srun() function to change the port and the address.

In the very first “Hello World” example, the server is started withapp.run(host='127.0.0.1',port=8080). To change the port, just pass a different port number to theport argument. To change the IP address which Bottle is listening on, just pass a different IP address to thehost argument.

Warning

It is highly recommendednot to run an application based on Bottle - or any web application - with Root / administrator rights! The whole code is excuted with elevated rights, which gives a (much) higher risk to harm the system in case of programming mistakes. Plus, in case an outside person can capture the application, e.g. by utilizes a bug in the code, this person may be able to work with elevated rights on the server. It is highly recommended to run Bottle with user rights, probably in case of a real application, by a dedicated user specifically set-up for this. In case the application should listen on a privileged port like 80 and / or 443, it is a common and a well-established practice to serve Bottle - or any WSGI-based application - with an WSGI application server with user rights on an unprivileged port locally and use a reverse proxy web server in front of the WSGI application server. More on this below.

Running Bottle with a different server

As said above, the build-in server is perfectly suitable for local development, personal use or a very small group of people within an internal network. For everything else, the development server may become a bottleneck, as it is single-threaded, thus it can only serve one request at a time. Plus, it may not be robust enough in general.

Bottle comes with a range ofserver adapters . To run the Bottle application with a different server than the build-in development server, simple pass theserver argument to the run function. For the following example, theWaitress WSGI application server from the Pylons project is used. Waitress works equally good on Linux, MacOS and Windows.

Note

Although Bottle comes with a variety of server adapters, each server except the build-in server must be installed separately. The servers arenot installed as a dependency of Bottle!

To install Waitress, go the venv in which Bottle is installed and run:

pip3installwaitress

To server the application via Waitress, just use Bottle’s server adapter for Waitress by changing theapp.run to:

app.run(host='127.0.0.1',port=8080,server='waitress')

After starting the application withpythontodo.py, a line in the output likeBottlev0.13.2serverstartingup(usingWaitressServer())... should be printed. Which confirms that the Waitress server instead of the WSGIRefServer is used.

This works exactly the same way with other servers supported by Bottle. However, there is one potential downside with this: it is not possible to pass any extra arguments to the server. Which may be necessary in many “real world” scenarios. A solution to that is shown in the next section.

Serving a Bottle App with a WSGI Application Server

Like any other Python WSGI framework, an application written with a Bottle has a so-called entry point, which can be passed to a WSGI Application server, which then serves the web application. In case of Bottle, the entry points is theapp instance created with the code lineapp=Bottle().

Sticking to Waitress (as used already in the previous section), serving the application works as follows:

waitress-servetodo:app

whereastodo is the name of the file holding the Bottle application andapp is the entry point, the instance of Bottle. Calling the WSGI application server directly allows to pass as many arguments to the server as need, e.g.

waitress-serve--listen:127.0.0.1:8080--threads=2todo:app

Final Words

This is the end of this tutorial for Bottle. The basic concepts of Bottle are shown and a first application utilizin the Bottle WSGI framework was written. Additionally, it was shown how to serve a Bottle application for real applications with a WSGI application server.

As said in the introduction, this tutorial is not showing all possibilities Bottle offers. What was skipped here is e.g. receiving file objects and streams and how to handle authentication data. For a complete overview of all features of Bottle, please refer to the fullBottle documentation .

Complete Example Listing

As the ToDo list example was developed piece by piece, here is the complete listing and the templates:

Main code for the applicationtodo.py:

importsqlite3frompathlibimportPathfrombottleimportBottle,template,request,redirectABSOLUTE_APPLICATION_PATH=Path(__file__).parents[0]app=Bottle()@app.route('/')defindex():redirect('/todo')@app.get('/todo')deftodo_list():show=request.query.showor'open'matchshow:case'open':db_query="SELECT id, task, status FROM todo WHERE status LIKE '1'"case'closed':db_query="SELECT id, task, status FROM todo WHERE status LIKE '0'"case'all':db_query="SELECT id, task, status FROM todo"case_:returntemplate('message.tpl',message='Wrong query parameter: show must be either open, closed or all.')withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute(db_query)result=cursor.fetchall()output=template('show_tasks.tpl',rows=result)returnoutput@app.route('/new',method=['GET','POST'])defnew_task():ifrequest.POST:new_task=request.forms.task.strip()withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("INSERT INTO todo (task,status) VALUES (?,?)",(new_task,1))new_id=cursor.lastrowidreturntemplate('message.tpl',message=f'The new task was inserted into the database, the ID is{new_id}')else:returntemplate('new_task.tpl')@app.route('/edit/<number:int>',method=['GET','POST'])defedit_task(number):ifrequest.POST:new_data=request.forms.task.strip()status=request.forms.status.strip()ifstatus=='open':status=1else:status=0withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("UPDATE todo SET task = ?, status = ? WHERE id LIKE ?",(new_data,status,number))returntemplate('message.tpl',message=f'The task number{number} was successfully updated')else:withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("SELECT task FROM todo WHERE id LIKE ?",(number,))current_data=cursor.fetchone()returntemplate('edit_task',current_data=current_data,number=number)@app.route('/details/<task:re:[0-9]+>')defshow_item(task):withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("SELECT task, status FROM todo WHERE id LIKE ?",(task,))result=cursor.fetchone()ifnotresult:returntemplate('message.tpl',message=f'The task number{item} does not exist!')else:returntemplate('message.tpl',message=f'Task:{result[0]}, status:{result[1]}')@app.route('/as_json/<number:re:[0-9]+>')deftask_as_json(number):withsqlite3.connect('todo.db')asconnection:cursor=connection.cursor()cursor.execute("SELECT id, task, status FROM todo WHERE id LIKE ?",(number,))result=cursor.fetchone()ifnotresult:return{'task':'This task IF number does not exist!'}else:return{'id':result[0],'task':result[1],'status':result[2]}@app.route('/static/<filepath:path>')defsend_static_file(filepath):ROOT_PATH=ABSOLUTE_APPLICATION_PATH/'static'returnstatic_file(filepath,root=ROOT_PATH)@app.error(404)defmistake404(error):return'Sorry, this page does not exist!'if__name__=='__main__':app.run(host='127.0.0.1',port=8080,debug=True,reloader=True)# remember to remove reloader=True and debug=True when moving# the application from development to a productive environment

Templatebase.tpl:

<!doctype html><htmllang="en-US"><head><metacharset="utf-8"/><title>ToDo App powered by Bottle</title></head><body>    {{!base}}</body></html>

Templateshow_tasks.tpl:

%#template to generate a HTML table from a list of tuples (or list of lists, or tuple of tuples or ...)% rebase('base.tpl')<p>The open ToDo tasks are as follows:</p><tableborder="1">%for row in rows:<tr>  %for col in row:<td>{{col}}</td>  %end</tr>%end</table><p><ahref="/new">Add a new task</a></p>

Templatemessage.tpl:

% rebase('base.tpl')<p>{{ message }}</p><p><ahref="/todo">Back to main page</p>

Templatenew_task.tpl:

%#template of the form for a new task% rebase('base.tpl')<p>Add a new task to the ToDo list:</p><formaction="/new"method="post"><p><inputtype="text"size="100"maxlength="100"name="task"></p><p><inputtype="submit"name="save"value="save"></p></form>

Templateedit_task.tpl:

%#template for editing a task%#the template expects to receive a value for "no" as well a "old", the text of the selected ToDo item<p>Edit the task with ID = {{no}}</p><formaction="/edit/{{no}}"method="get"><inputtype="text"name="task"value="{{old[0]}}"size="100"maxlength="100"><selectname="status"><option>open</option><option>closed</option></select><br><inputtype="submit"name="save"value="save"></form>