Using the Bottle framework to build Python apps

Python in a Bottle

Article from Issue 174/2015

The Bottle framework provides the fastest and easiest way to write web apps in Python. In this article, we help you get started with this lightweight framework.

Python lets you quickly whip up simple and more advanced standalone applications, even if your coding skills are relatively modest. What if you want to build a Python-based web app, though? Several frameworks let you do that, and if you are looking for something simple and lightweight, Bottle [1] is exactly what you need. This micro framework offers just the right mix of functionality and simplicity, which makes it an ideal tool for building Python-based web apps with consummate ease.

Installing Bottle

The easiest way to install Bottle is using the Python Package Manager (also known as pip). It's available in the official software repositories of many mainstream Linux distributions, so it can be installed using the default package manager. To deploy pip on Debian or Ubuntu, run

apt-get install python-pip

as root and then install Bottle using

pip install bottle

as root.

Bottle Basics

A typical Bottle app consists of several functions, each performing a specific task. Usually, the result returned by a function is used as the dynamic content for generating pages. Each function has a so-called route, or an address on the server. When the browser calls this address, it triggers the function. The simple Bottle app below demonstrates how this works in practice:

from bottle import route, run
def hello():
    return "Hello World!"
run(host='localhost', port=8080)

The first statement imports the route and run modules that are used to define routes and run the app. The app itself consists of a single hello() function, which returns the "Hello World!" message. The /hello route specifies the app's address, and the run() routine makes the app accessible on port 8080 of the localhost.

To run this simple app, create a text file, paste the code above into it, and save the file as Make the script executable using the

chmod +x

command, then run the server by issuing ./ Point the browser to the http://localhost:8080/hello address, and you should see the "Hello World!" message.

What's in My Bag?

To demonstrate Bottle's basics, I'll build a simple web app called What's in My Bag (or wimb for short) that can be used for keeping tabs on the contents of your bag. The app uses an SQLite database to store data, and it allows you to add, edit, and remove records. Each record consists of three fields: id (a unique identifier), item (the item's description), and serial_no (the serial number of the item). The core of the app is the wimb() function shown in Listing 1.

Listing 1

The wimb() Function

01 #!/usr/bin/python
02 import sqlite3, os
03 from bottle import route, redirect, run, debug, template, \
   request, static_file
04 @route('/wimb')
05 def wimb():
06     if os.path.exists('wimb.sqlite'):
07         conn = sqlite3.connect('wimb.sqlite')
08         c = conn.cursor()
09         c.execute("SELECT id,item, serial_no FROM wimb")
10         result = c.fetchall()
11         c.close()
12         output = template('wimb.tpl', rows=result)
13         return output
14     else:
15         conn = sqlite3.connect('wimb.sqlite')
16         conn.execute("CREATE TABLE wimb (id INTEGER PRIMARY KEY, \
                        item char(254) NOT NULL, serial_no char(100))")
17         conn.commit()
18         return redirect('/wimb')

This function does several things. It starts by checking whether the database wimb.sqlite exists. If not, the function creates it; otherwise, the function establishes a connection to the database and fetches the records from the wimb table. To show the fetched data as a properly formatted table, the function uses the template specified in the output statement in line 12.

A template in Bottle is a regular text file with the .tpl extension. A template can contain any text (including HTML markup), additional Python code, and arguments (e.g., the result of a database query). For example, the following simple template takes the record set returned by the wimb function and formats it as an HTML table.

<table border="1">
%for row in rows:
  %for col in row:

Although this template does the job, it also has a couple of limitations. Apart from the fact that it's rather bare-bones, the template doesn't give you control over individual columns, so you can't, for example, apply different styles to individual columns. More importantly, the template provides no way to edit and delete records. The extended template in Listing 2 addresses these limitations.

Listing 2

Extended Template

01 <h1>What's in My Bag:</h1>
02 <table border="0">
03 <tr><th>ID</th><th>Item</th><th>Serial no.</th></tr>
04 %for row in rows:
05     %id = row[0]
06     %item = row[1]
07     %serial_no = row[2]
08     <tr>
09     <td>{{id}}</td>
10     <td>{{item}}</td>
11     <td>{{serial_no}}</td>
12     <td><a href="/edit/{{id}}">Edit</a></td>
13     <td><a href="/delete/{{id}}">Delete</a></td>
14   </tr>
15 %end
16 </table>

Each row in the result set contains a list of columns, and the template uses simple Python code to assign values of individual columns to separate variables. These variables are then used as arguments in the HTML table. In this way, you can style columns individually. For example, you can create the following CSS class:

td.col1 {
    color: #3399ff;

Then you can assign this class to the first column to apply the specified styling:

<td class="col1">{{id}}</td>

The id variable is also used as an argument in links for editing and deleting records (more about this later). To be able to add records, the app needs another function and template. The latter is a simple HTML form (Listing 3) consisting of two input text fields (one for the item description and another for the serial number) and a submit button.

Listing 3


01 <h1>Add a new item:</h1>
02 <form action="/add" method="GET">
03 <p><input type="text" size="50" maxlength="254" name="item"></p>
04 <p><input type="text" size="50" maxlength="100" name="serial_no"></p>
05 <p><input type="submit" name="add" value="Add"></p>
06 </form>

When the Add button is pressed, the values entered in the fields are sent to the dedicated function that processes the values and inserts them into the database (Listing 4).

Listing 4

Adding a Record

01 @route('/add', method='GET')
02 def new_item():
03     if request.GET.get('add','').strip():
04         item = request.GET.get('item', '').strip()
05         serial_no = request.GET.get('serial_no', '').strip()
06         conn = sqlite3.connect('wimb.sqlite')
07         c = conn.cursor()
08         c.execute("INSERT INTO wimb (item,serial_no) VALUES \
                     (?,?)", (item,serial_no))
09         new_id = c.lastrowid
10         conn.commit()
11         c.close()
12         return redirect('/wimb')
13     else:
14         return template('add_item.tpl')

To obtain values from the form's fields, the function uses the statements in lines 4 and 5. Once the function has done its job, it redirects to the app's root using the return statement in line 12. All these actions are performed when the user presses the Add button in the form; otherwise, the function simply displays the appropriate template (i.e., an empty form).

The app uses yet another function and template for editing and updating existing records. Each record in the database has a unique identifier, which is used to fetch the correct record and its data and then save the changes made to it. If you take a look at the edit_item() function shown in Listing 5, you'll notice that its route contains the :no variable. This is a so-called dynamic route, in which the value of the variable is a part of the route.

Listing 5

The edit_item() Function

01 @route('/edit/:no', method='GET')
02 def edit_item(no):
03     if request.GET.get('save','').strip():
04         item = request.GET.get('item','').strip()
05         serial_no = request.GET.get('serial_no','').strip()
06         conn = sqlite3.connect('wimb.sqlite')
07         c = conn.cursor()
08         c.execute("UPDATE wimb SET item = ?, serial_no = ? \
                     WHERE id LIKE ?", (item, serial_no, no))
09         conn.commit()
10         return redirect('/wimb')
11     else:
12         conn = sqlite3.connect('wimb.sqlite')
13         c = conn.cursor()
14         c.execute("SELECT item,serial_no FROM wimb WHERE id LIKE ?", \
15         cur_data = c.fetchone()
16         return template('edit_item.tpl', old=cur_data, no=no)

The value of the variable is also passed to the function assigned to that route, and this value can be processed by the function. In this particular case, the variable contains the ID number of the target record, so when you call the /edit/1 address, the function fetches the records with ID 1, obtains its existing values, and inserts them into the appropriate fields of the edit_item.tpl template. Once you've edited the data and pressed the Save button, the function obtains the modified values of the form's fields and updates the appropriate record.

Similar to the edit_item() function, the edit_item.tpl template in Listing 6 uses the {{no}} argument to determine the record's ID along with arguments for populating the fields with existing values from the record.

Listing 6

The edit_item.tpl Template

01 <h1>Edit item number {{no}}</h1>
02 <form action="/edit/{{no}}" method="GET">
03 <p><input type="text" name="item" value="{{old[0]}}" \
   size="50" maxlength="254"></p>
04 <p><input type="text" name="serial_no" value="{{old[1]}}" \
   size="50" maxlength="100"></p>
05 <p><input type="submit" name="save" value="Save"></p>
06 </form>

Deleting existing records is the final piece of the puzzle. Here, too, the app uses a combination of a function and a template. The delete_item() function in Listing 7 is basically a simplified version of the edit_item() function.

Listing 7

The delete_item() Function

01 @route('/delete/:no', method='GET')
02 def delete_item(no):
03     if request.GET.get('delete','').strip():
04         conn = sqlite3.connect('wimb.sqlite')
05         c = conn.cursor()
06         c.execute("DELETE FROM wimb WHERE id LIKE ?", (no))
07         conn.commit()
08         return redirect('/wimb')
09     else:
10         return template('delete_item.tpl', no=no)

This function uses the :no variable to pick the right record and deletes the record when the user presses the Delete button in the delete_item.tpl template:

<h1>Delete item number {{no}}?</h1>
<form action="/delete/{{no}}" method="GET">
<input type="submit" name="delete" value="Delete">

So far, all content in the app has been generated dynamically. What if you want to include static content, though? For example, you might want to prettify the templates using styles defined in a separate CSS stylesheet. To do this, you need to specify yet another function that defines the path to the static content (in this case, it's any file and folder inside the static directory):

@route('/static/:path#.+#', name='static')
def static(path):
    return static_file(path, root='static')

Then you would put your CSS stylesheet into the static directory and the reference to it in the templates in the usual manner:

<link rel="stylesheet" type="text/css" href="static/styles.css">

Figure 1 shows what the app looks like.

Figure 1: What's in My Bag app.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus

Direct Download

Read full article as PDF:

Price $2.95