Nothing Special   »   [go: up one dir, main page]

Build a Contact Book App With Python, Textual, and SQLite

Build a Contact Book App With Python, Textual, and SQLite

by Leodanis Pozo Ramos Oct 09, 2024 intermediate projects

Building projects is a great way to learn programming and have fun at the same time. When you work on a project, you apply different coding skills simultaneously, which is good practice for what you’ll do in a real-life project. In this tutorial, you’ll create a contact book application with a text-based interface (TUI) based on Python and Textual. To store the contact data, your app will use an SQLite database.

In this tutorial, you’ll learn how to:

  • Create the contact book app’s TUI using Textual
  • Handle the database operations using SQLite
  • Connect the app’s TUI with the database code and make it functional

At the end of this project, you’ll have a functional contact book application that will allow you to store and manage your contact information.

To get the complete source code for the application and the code for every step in this tutorial, click the link below:

Demo: A Contact Book Built With Python and Textual

Contact or address books are a widely used type of application. They can be found on phones and computers, allowing users to store and manage contact information for family, friends, coworkers, and so on.

In this tutorial, you’ll code a contact book TUI app with Python, Textual, and SQLite. Here’s a demo of how your contact book will look once you’ve followed all the steps:

Your contact book will provide a basic set of features for this type of application, and you’ll be able to display, add, and remove the information in your contacts list.

Project Overview

To build your contact book app, you’ll organize the code in a few modules under a package. In this tutorial, you’ll use the following directory structure:

rpcontacts_project/
│
├── rpcontacts/
│   ├── __init__.py
│   ├── __main__.py
│   ├── database.py
│   ├── rpcontacts.tcss
│   └── tui.py
│
├── README.md
└── requirements.txt

The root directory of your project is rpcontacts_project/. Inside, there’s an rpcontacts/ subdirectory that holds the application’s main package.

You’ll cover the content of each file in this tutorial. The name of each file will give you an idea of its role in the application.

For example, __main__.py will host the application, and database.py will provide database-related code. Similarly, rpcontacts.tcss is a CSS file that will allow you to tweak the visual style of your Textual app. Finally, tui.py will contain the code to generate the app’s TUI, including the main screen and a couple of auxiliary screens or dialogs.

Prerequisites

To get the most out of this project, you should have some previous knowledge of how to lay out a Python project and work with SQLite databases. You should also know the basics of working with Python classes. Some knowledge about writing CSS code would also be a plus.

To satisfy these knowledge requirements, you can take a look at the following resources:

Don’t worry if you don’t have all of the prerequisite knowledge before starting this tutorial—that’s completely okay! You’ll learn through the process of getting your hands dirty as you build the project. If you get stuck, then take some time to review the resources linked above. Then, get back to the code.

The contact book application you’ll build in this tutorial has a single external dependency, which is Textual. This library provides a rapid application development framework that allows you to create apps you can run in your terminal and browser.

To follow best practices in your development process, you can start by creating a virtual environment and then install Textual using pip:

Windows PowerShell
PS> mkdir rpcontacts_project\
PS> cd rpcontacts_project\
PS> python -m venv venv\
PS> venv\Scripts\activate
(venv) PS> python -m pip install textual
Shell
$ mkdir rpcontacts_project/
$ cd rpcontacts_project/
$ python -m venv venv/
$ source venv/bin/activate
(venv) $ python -m pip install textual

Once you’ve run these commands, you can launch your favorite code editor or IDE in the project’s root directory and you’re ready to start coding!

Step 1: Create the Contact Book’s App With Textual

In this first step, you’ll create a minimal TUI application to provide the foundation on which you’ll start to build the contact book. You’ll also create the required project structure.

All of the code and files you’ll add to the contact book project in this section are in the source_code_step_1/ directory. You can download them by clicking the link below:

By the end of this section, you’ll be able to run the skeleton TUI application for your contact book for the first time.

Structure the Contact Book Project

In the previous section, you created a new directory called rpcontacts_project/ as the project’s root directory. Now, you’ll create a new subdirectory called rpcontacts/ inside rpcontacts_project/. This subdirectory will hold the application’s main package.

To turn a directory into a package, Python needs a __init__.py module to initialize the package. So, create this file within rpcontacts/ and add the following code to it:

Python rpcontacts/__init__.py
__version__ = "0.1.0"

This file tells Python that rpcontacts/ is a package. The code in the file runs when you import the package or some of its modules. You don’t need to put any code in a __init__.py file to initialize the package. An empty __init__.py file will do the job as well. However, in this case, you define a module-level constant called __version__ to hold the version number of your application.

Now go ahead and create the rest of the files in rpcontacts_project/ and its rpcontacts/ subdirectory.

Create the App’s Main TUI

It’s time to write some code for your contact book’s main screen. To do that, go to your code editor and create the tui.py file inside rpcontacts/. Once there, add the following code:

Python rpcontacts/tui.py
from textual.app import App
from textual.widgets import Footer, Header

class ContactsApp(App):
    def compose(self):
        yield Header()
        yield Footer()

    def on_mount(self):
        self.title = "RP Contacts"
        self.sub_title = "A Contacts Book App With Textual & Python"

First, you import the required classes from textual. Then, you create ContactsApp by inheriting from the App class, which is the base class for Textual applications.

Next, you add a method called .compose(). Textual automatically calls this method when you run the app. With this method, you can build the app’s main screen. For now, your app will only have a header and a footer, which you’ll create with the Header and Footer classes that are built into Textual.

Finally, you add the .on_mount() method, which is also called automatically. In this method, you set up some properties of your main screen, like the title and subtitle. With this, you have the skeleton TUI for your app.

Code and Run the App

Now you need to create the app’s entry-point script, which will live in the __main__.py file under rpcontacts/. Go ahead and create and open the file. Then, add the following code:

Python rpcontacts/__main__.py
from rpcontacts.tui import ContactsApp

def main():
    app = ContactsApp()
    app.run()

if __name__ == "__main__":
    main()

In this code, you first import the ContactsApp from your tui module. Then, you create a main() function where you instantiate ContactsApp and call its .run() method.

Finally, you have the name-main idiom, which allows you to run the app’s main() function when the containing file is run as an executable program.

Now you can run the following command to execute your app for the first time:

Shell
$ python -m rpcontacts

Once you run this command, you’ll get the following screen on your terminal:

Contact Book Skeleton Textual App

At the top of this terminal window, you can see your app’s header with the title and subtitle. At the bottom, you’ll see a gray bar, which is the app’s footer. To close the app, you can press Ctrl+C on your keyboard.

Add Actions to the App’s Skeleton TUI

You already have your contact book’s main TUI set up. However, at this point, the TUI doesn’t do much. What do you think about adding a couple of actions? An action is something you can do by pressing a key while your app’s main screen is active. In a TUI app, you normally add actions to the footer panel.

To kick thing off, you’ll start by adding the following two actions:

  1. A toggle dark action to toggle between dark and light modes.
  2. An exit action to close the application.

Go back to the tui.py file and add the following code to provide the toggle dark action:

Python rpcontacts/tui.py
from textual.app import App
from textual.widgets import Footer, Header

class ContactsApp(App):
    BINDINGS = [
        ("m", "toggle_dark", "Toggle dark mode"),
    ]

    def compose(self):
        yield Header()
        yield Footer()

    def on_mount(self):
        self.title = "RP Contacts"
        self.sub_title = "A Contacts Book App With Textual & Python"

    def action_toggle_dark(self):
        self.dark = not self.dark

To add an action, you can use a binding, which is a tuple containing the action’s key, name, and text. To define bindings, you can add the BINDINGS constant to your app’s main class as you did in the first group of highlighted lines. For the toggle dark action, you’ll use the M key. The action’s name is "toggle_dark", and the text is "Toggle dark mode".

The next step is to code the method that your app must invoke when you press the action key. In this example, the method is called .action_toggle_dark(). Note that the method name must start with .action_ and continue with the binding’s name. This way, Textual will know which method to call when you press the M key.

Inside the .action_toggle_dark() method, you use the Boolean not operator to toggle the value of the .dark attribute, which your ContactsApp inherits from App.

Here’s how the toggle dark action works now:

As you can see, the actions are conveniently displayed on the app’s footer so that you can quickly identify them. When you press the M key, the app toggles from dark to light mode and vice versa.

Now that you know the basics of adding actions with Textual, you can add the exit action. To do this, you must first code a screen that works as a confirmation dialog. In other words, you need a dialog to ask the user whether they want to close the app.

You can inherit from the Screen class to create a new screen to use as a dialog. The screen must have a confirmation message, which you can pass to your class’s constructor. It also needs Yes and No buttons.

Back in the tui.py file, update the imports as shown below:

Python rpcontacts/tui.py
from textual.app import App
from textual.containers import Grid
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label

# ...

In this code, you import the Grid container to arrange the widgets or graphical components on the screen. You also import the Screen, Button, and Label classes, which you’ll use to create the screen, provide the required buttons, and display the confirmation message.

Here’s the code that implements the Question dialog:

Python rpcontacts/tui.py
 1# ...
 2
 3class QuestionDialog(Screen):
 4    def __init__(self, message, *args, **kwargs):
 5        super().__init__(*args, **kwargs)
 6        self.message = message
 7
 8    def compose(self):
 9        no_button = Button("No", variant="primary", id="no")
10        no_button.focus()
11
12        yield Grid(
13            Label(self.message, id="question"),
14            Button("Yes", variant="error", id="yes"),
15            no_button,
16            id="question-dialog",
17        )
18
19    def on_button_pressed(self, event):
20        if event.button.id == "yes":
21            self.dismiss(True)
22        else:
23            self.dismiss(False)

Here’s a breakdown of what this code does:

  • Line 3 defines the QuestionDialog class by inheriting from Screen.
  • Line 4 defines the class’s initializer, which takes a message argument that you’ll use to pass in the confirmation message.
  • Line 5 calls the initializer of Screen using super().
  • Line 6 defines an instance attribute to hold the confirmation message.
  • Line 8 defines the .compose() method where you’ll define the dialog’s TUI.
  • Line 9 defines the No button using the Button class. To build the button, you pass the button’s text, variant, and id arguments. On line 10, you call the .focus() method on the button object to put the focus on this button by default.
  • Lines 12 to 17 yield a Grid object containing the widgets.
  • Lines 19 to 23 define the .on_button_pressed() method. Textual will call this method automatically when you press the buttons on the screen. This method takes an event object as an argument. Then, you check whether the pressed button was the Yes button. If that’s the case, then you call .dismiss() to return True from the dialog. Otherwise, you call .dismiss() to return False.

This code will work. However, the resulting dialog will need to be styled better. Here’s where Textual’s CSS comes in handy. Create and open the rpcontacts.tcss file to style the dialog using CSS. Once there, add the following code:

CSS rpcontacts/rpcontacts.tcss
QuestionDialog {
    align: center middle;
}

#question-dialog {
    grid-size: 2;
    grid-gutter: 1 2;
    grid-rows: 1fr 3;
    padding: 0 1;
    width: 60;
    height: 11;
    border: solid red;
    background: $surface;
}

#question {
    column-span: 2;
    height: 1fr;
    width: 1fr;
    content-align: center middle;
}

Button {
    width: 100%;
}

In this CSS code, you first define a rule for the QuestionDialog class. In that rule, you set the align property to center middle to tell Textual to display the dialog centered vertically and horizontally.

Then, you define the #question-dialog styling rule to tweak several properties on the dialog. With the #question rule, you customize how the confirmation message will display in the dialog. Finally, you define a rule for all of the buttons on the app’s TUI. In this example, you want all the buttons to have a width of 100%.

To link this CSS file with your app’s code, add the CSS_PATH constant to ContactsApp. This constant must provide the path to the rpcontacts.tcss file. To do this, go back to tui.py and update it as shown below:

Python rpcontacts/tui.py
from textual.app import App
from textual.containers import Grid
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, Label

class ContactsApp(App):
    CSS_PATH = "rpcontacts.tcss"
    BINDINGS = [
        ("m", "toggle_dark", "Toggle dark mode"),
    ]

# ...

That’s it! Your app now has an associated CSS file that you can use to tweak the appearance of the TUI. You’ll come back to this CSS file in upcoming sections. For now, you’re all set up.

To launch the Question dialog when you press the exit action’s associated key, you need to define a new binding and its associated method. Go ahead and update your tui.py file with the following code:

Python rpcontacts/tui.py
# ...

class ContactsApp(App):
    CSS_PATH = "rpcontacts.tcss"
    BINDINGS = [
        ("m", "toggle_dark", "Toggle dark mode"),
        ("q", "request_quit", "Quit"),
    ]

    def compose(self):
        yield Header()
        yield Footer()

    def on_mount(self):
        self.title = "RP Contacts"
        self.sub_title = "A Contacts Book App With Textual & Python"

    def action_toggle_dark(self):
        self.dark = not self.dark

    def action_request_quit(self):
        def check_answer(accepted):
            if accepted:
                self.exit()

        self.push_screen(QuestionDialog("Do you want to quit?"), check_answer)

# ...

In the first highlighted line, you add a new binding to launch the quit action. You’ll use the Q key to access this action.

The .action_request_quit() does all the hard work. Inside this method, you create an inner function to process the user’s response from the Question dialog. If the response is True, then you exit the app by calling its .exit() method.

At the end of the method, you call the .push_screen() method to launch the dialog. This method takes a screen instance as its first argument. The second argument should be the function object that you’ll use to process the dialog’s response, which is check_answer() in this example.

With all this code in place, you can run the app again to make sure that the exit action works correctly. Your app will work as shown below:

When you press the Q key, your app launches a dialog to confirm whether you want to close the app. If you click the No button, then nothing happens. If you click Yes, then the app terminates, and you’ll be back in your terminal session.

Step 2: Build the Contact Book’s TUI

Now that you’ve built the skeleton of your contact book app, you can start coding the main TUI. In this project, the app’s TUI will look as shown below:

Contact Book Planned Main TUI

In this TUI, you have two main components. On the left side, you have the contacts list, which includes the contact’s name, phone number, and email address. On the right side, you have a panel with three buttons:

  1. Add to add a new contact to the list
  2. Delete to remove the selected contact from the list
  3. Clear All to remove all contacts from the list

All of the code and files you’ll add or modify in this section are in the source_code_step_2/ directory. You can download them by clicking the link below:

Get back to your tui.py module and update the code of ContactsApp to build the desired TUI:

Python rpcontacts/tui.py
from textual.app import App
from textual.containers import Grid, Horizontal, Vertical
from textual.screen import Screen
from textual.widgets import (
    Button,
    DataTable,
    Footer,
    Header,
    Label,
    Static,
)

class ContactsApp(App):
    CSS_PATH = "rpcontacts.tcss"
    BINDINGS = [
        ("m", "toggle_dark", "Toggle dark mode"),
        ("a", "add", "Add"),
        ("d", "delete", "Delete"),
        ("c", "clear_all", "Clear All"),
        ("q", "request_quit", "Quit"),
    ]

    def compose(self):
        yield Header()
        contacts_list = DataTable(classes="contacts-list")
        contacts_list.focus()
        contacts_list.add_columns("Name", "Phone", "Email")
        contacts_list.cursor_type = "row"
        contacts_list.zebra_stripes = True
        add_button = Button("Add", variant="success", id="add")
        add_button.focus()
        buttons_panel = Vertical(
            add_button,
            Button("Delete", variant="warning", id="delete"),
            Static(classes="separator"),
            Button("Clear All", variant="error", id="clear"),
            classes="buttons-panel",
        )
        yield Horizontal(contacts_list, buttons_panel)
        yield Footer()

    # ...

You first import some extra Textual classes to use in the TUI. The newly imported classes are:

  • Horizontal and Vertical: Let you arrange the widgets on the TUI.
  • DataTable: Provides a table-like view that you’ll use to display the contacts list.
  • Static: Lets you create a separator widget to separate the buttons on the right panel.

Inside ContactsApp, the first addition is a series of new bindings that you’ll implement in your application later on.

The most relevant additions to the class are in the .compose() method. After the header object, you create an instance of DataTable. This instance will use a CSS rule called contacts-list. You’ll write this CSS class in a moment.

The table will have three columns:

  1. Name displays the contact’s name.
  2. Phone displays the contact’s phone number.
  3. Email displays the contact’s email address.

Next, you set the cursor type to "row". This way, when you click the list of contacts, the cursor will highlight the selected row. The .zebra_stripes property makes the table display rows using alternate colors, which is a visually appealing effect.

The next step is to create the Add button and put focus on it. Once the button is set up, you create a vertical container to define the panel of buttons. This container will hold the Add and Delete buttons, a separator, and the Clear All button. Again, this container will use the buttons-panel CSS rule that you’ll define in a moment.

To complete the TUI layout, you yield a horizontal container with the contacts list and the panel of buttons.

Now you need to write the required CSS code to style the app’s TUI and make it look more stylized. Go back to the rpcontacts.tcss file and update it as shown below:

CSS rpcontacts/rpcontacts.tcss
/* ... */

.contacts-list {
    width: 3fr;
    padding: 0 1;
    border: solid green;
}

.buttons-panel {
    align: center top;
    padding: 0 1;
    width: auto;
    border: solid red;
}

.separator {
    height: 1fr;
}

In this code snippet, you have three new CSS rules. The contacts-list rule sets a few properties to format the table of contacts. The buttons-panel rule styles the panel of buttons, and the separator rule allows you to format the separator you place between the buttons.

With these additions to your code, you can run the application again. The app’s TUI on your terminal window will look like the one you saw at the beginning of this section:

Contact Book Planned Main TUI

So far, you’ve run all the required steps to create the TUI of your contact book application. Now, you’re ready to start working on how your application will manage and store your contacts’ data.

Step 3: Set Up the Contact Book’s Database

At this point, you’ve created a Textual app with an appropriate TUI to build a contact book. In this section, you’ll write code to operate an SQLite database where you’ll store the contact information persistently.

The source code and files you’ll add or modify in this section are stored in the source_code_step_3/ directory. You can download them by clicking the link below:

Create a Database Class

Your contacts database will have a single table that you’ll call contacts. This table will have the following columns:

Column Description
id An auto-generated integer primary key
name A string containing the name of a contact
phone A string representing the phone number of a contact
email A string containing the email of a contact

The contacts table in your database will store all the relevant information about your contacts. To group the database operations, you’ll create a Database class. Go ahead and create the database.py file and open it in your code editor. Then, add the following code:

Python rpcontacts/database.py
import pathlib
import sqlite3

DATABASE_PATH = pathlib.Path().home() / "contacts.db"

class Database:
    def __init__(self, db_path=DATABASE_PATH):
        self.db = sqlite3.connect(db_path)
        self.cursor = self.db.cursor()
        self._create_table()

In this code snippet, you import pathlib to handle the path to the database file and sqlite3 to operate the database itself. The DATABASE_PATH constant holds the default path to the database file on your hard drive. In this example, the database will live in your home directory, and the filename will be contacts.db. You can change this configuration to better suit your preferences if you’d like.

Next, you define the Database class. The class initializer takes the path to the database file as an argument. You use this path to connect to the database using the connect() function from the sqlite3 module.

Once you have an active connection to the database, you get a cursor to run SQL queries on the database. Finally, you call the ._create_table() non-public method. Here’s the implementation of this method:

Python rpcontacts/database.py
# ...

class Database:
    # ...

    def _create_table(self):
        query = """
            CREATE TABLE IF NOT EXISTS contacts(
                id INTEGER PRIMARY KEY,
                name TEXT,
                phone TEXT,
                email TEXT
            );
        """
        self._run_query(query)

    def _run_query(self, query, *query_args):
        result = self.cursor.execute(query, [*query_args])
        self.db.commit()
        return result

In ._create_table(), you create an SQL query using a triple-quoted string. The first time this query runs, it will create a table called contacts on a new database file called contacts.db. Note that this query doesn’t override an existing database or table because of the IF NOT EXISTS condition. The table will have the four columns that you planned before.

Next, you call the ._run_query() helper method. This method takes an SQL query and a list of query parameters as arguments. Then, it executes the query against the database, commits the changes, and returns the query result. This method will come in handy when you implement other database operations, which is what you’ll be doing next.

Implement the Database Operations

With the initial Database implementation in place, you can start coding the database operations that are directly related to the features of your contact book app. In this example, your app will need to do the following actions on the database:

  • Retrieve all the contacts
  • Get the last contact
  • Add a new contact
  • Delete an existing contact
  • Clear all contacts

Here’s the code that implements all of these database operations:

Python rpcontacts/database.py
# ...

class Database:
    # ...

    def get_all_contacts(self):
        result = self._run_query("SELECT * FROM contacts;")
        return result.fetchall()

    def get_last_contact(self):
        result = self._run_query(
            "SELECT * FROM contacts ORDER BY id DESC LIMIT 1;"
        )
        return result.fetchone()

    def add_contact(self, contact):
        self._run_query(
            "INSERT INTO contacts VALUES (NULL, ?, ?, ?);",
            *contact,
        )

    def delete_contact(self, id):
        self._run_query(
            "DELETE FROM contacts WHERE id=(?);",
            id,
        )

    def clear_all_contacts(self):
        self._run_query("DELETE FROM contacts;")

The first two methods allow you to get data from the database. To do this, they use appropriate SELECT queries, call ._run_query(), and return the required data by calling the appropriate method on the query result.

The .add_contact() method takes contact as an argument. This argument must be a tuple of the form (name, phone, email). Then, the method runs an INSERT INTO query to add the contact’s information to the database.

The .delete_contact() method takes a contact’s ID as an argument and removes the contact’s information from the database using an SQL query and the ._run_query() helper method.

Finally, .clear_all_contacts() runs an SQL query that removes all the data from your contacts.db database.

Try Out the Database Class

You’ve finished writing the required code that will handle the connection to the contact book’s database. In this section, you’ll run some sample code to make sure that the database works properly.

Open your command line and navigate to the project’s root directory, rpcontacts_project/. Once there, launch a Python interactive session and type in the following code:

Python
>>> from rpcontacts.database import Database
>>> db = Database()

>>> data = [
...     ("Linda", "111-2222-3333", "linda@example.com"),
...     ("Joe", "111-2222-3333", "joe@example.com"),
...     ("Lara", "111-2222-3333", "lara@example.com"),
...     ("David", "111-2222-3333", "david@example.com"),
...     ("Jane", "111-2222-3333", "jane@example.com"),
... ]

>>> for contact in data:
...     db.add_contact(contact)
...

>>> db.get_all_contacts()
[
    (1, 'Linda', '111-2222-3333', 'linda@example.com'),
    (2, 'Joe', '111-2222-3333', 'joe@example.com'),
    (3, 'Lara', '111-2222-3333', 'lara@example.com'),
    (4, 'David', '111-2222-3333', 'david@example.com'),
    (5, 'Jane', '111-2222-3333', 'jane@example.com')
]

>>> db.delete_contact(5)
>>> db.delete_contact(4)

>>> db.get_all_contacts()
[
    (1, 'Linda', '111-2222-3333', 'linda@example.com'),
    (2, 'Joe', '111-2222-3333', 'joe@example.com'),
    (3, 'Lara', '111-2222-3333', 'lara@example.com')
]

Your database works great! You now have some sample data to play with to test the application. So, next you can focus on how to load and display the contact information on your contact book’s TUI.

Step 4: Display Existing Contacts on the App’s TUI

To display your contacts’ data in the application’s main TUI, you first need to load that information from the database. Then, you need a way to display the retrieved data in the contacts list. You already have a way to get the contacts’ data from the database using the Database class and its .get_all_contacts() method. You only need to pass an instance of this class to ContactsApp.

Go back to the tui.py file and add the following code right after the BINDINGS constant:

Python rpcontacts/tui.py
# ...

class ContactsApp(App):
    # ...

    def __init__(self, db):
        super().__init__()
        self.db = db

    # ...

In this code snippet, you add an .__init__() method to your ContactsApp class. This method takes a Database instance as an argument. Then, it initializes the parent class, App, and creates an instance attribute to hold a reference to the database.

With this code in place, open the __main__.py file and modify it as shown below:

Python rpcontacts/__main__.py
from rpcontacts.database import Database
from rpcontacts.tui import ContactsApp

def main():
    app = ContactsApp(db=Database())
    app.run()

if __name__ == "__main__":
    main()

Here, you import Database from the database module and then pass an instance of this class to the ContactsApp() constructor.

The files and the code you’ll add or modify in this section are stored in the source_code_step_4/ directory. To download them, click the link below:

Now get back to the tui.py file, add the following code to the file, and save it:

Python rpcontacts/tui.py
# ...

class ContactsApp(App):
    # ...

    def on_mount(self):
        self.title = "RP Contacts"
        self.sub_title = "A Contacts Book App With Textual & Python"
        self._load_contacts()

    def _load_contacts(self):
        contacts_list = self.query_one(DataTable)
        for contact_data in self.db.get_all_contacts():
            id, *contact = contact_data
            contacts_list.add_row(*contact, key=id)

    # ...

In this code, you first update the .on_mount() method by adding a call to ._load_contacts() at the end. Inside this helper method, you get a reference to your contacts list using the .query_one() method inherited from App.

Next, you start a for loop over the contacts currently stored in the database. To get this list, you call .get_all_contacts() on the database instance. Then, you unpack the data of each contact into two variables:

  1. id holds the contact’s ID in the database.
  2. contact holds the contact’s information as a tuple of the form (name, phone, email).

To display the data on the DataTable representing the list of contacts on the app’s TUI, you use the .add_row() method. To provide the data to add to each row, you unpack the content of contact. Finally, you pass the contact’s ID as the key argument. You’ll use this key later on when you implement the contact deletion functionality.

With this code in place, you can run the app again. You’ll get something like the following in your terminal window:

Contact Book Display Contacts

Great! Your contact book application is able to display the existing contacts in the contacts list. Now, you can move on and write the code that will allow you to add new contacts to your database.

Step 5: Add New Contacts to the Database and TUI

At this point, your contact book application has an appealing TUI that can load and display the information about your existing contacts. In this section, you’ll write the code required to add new contacts to the database. The first step in accomplishing this is to create the Add Contact dialog.

All the files and the code you’ll add or modify in this section are in the source_code_step_5/ directory. To download them, click the link below:

Create the Add Contact Dialog

In this section, you’ll create a new Textual screen that will be a dialog for adding contacts to your database. The Add Contact dialog will have the required widgets to allow users to enter new contact information. It will look something like the following:

Contact Book Add Contact Dialog

In this dialog, you’ll use Label, Input, and Button objects. The Input class lets you create text input widgets to take the user’s input.

To code the Add Contact dialog, you’ll subclass Screen as you did with the Question dialog. Open the tui.py module and update it like in the following code snippet:

Python rpcontacts/tui.py
 1# ...
 2from textual.widgets import (
 3    Button,
 4    DataTable,
 5    Footer,
 6    Header,
 7    Input,
 8    Label,
 9    Static,
10)
11
12# ...
13
14class InputDialog(Screen):
15    def compose(self):
16        yield Grid(
17            Label("Add Contact", id="title"),
18            Label("Name:", classes="label"),
19            Input(
20                placeholder="Contact Name",
21                classes="input",
22                id="name",
23            ),
24            Label("Phone:", classes="label"),
25            Input(
26                placeholder="Contact Phone",
27                classes="input",
28                id="phone",
29            ),
30            Label("Email:", classes="label"),
31            Input(
32                placeholder="Contact Email",
33                classes="input",
34                id="email",
35            ),
36            Static(),
37            Button("Cancel", variant="warning", id="cancel"),
38            Button("Ok", variant="success", id="ok"),
39            id="input-dialog",
40        )
41
42    def on_button_pressed(self, event):
43        if event.button.id == "ok":
44            name = self.query_one("#name", Input).value
45            phone = self.query_one("#phone", Input).value
46            email = self.query_one("#email", Input).value
47            self.dismiss((name, phone, email))
48        else:
49            self.dismiss(())

There are a lot of things happening in this code. Here’s a summary:

  • Line 7 imports the Input class, which you’ll use to allow the user to enter the contact’s information.
  • Line 14 defines the InputDialog class inheriting from Screen.
  • Lines 15 to 40 set up the dialog’s TUI with the required widgets. You have a label for the dialog’s title. Then, you have labels and input widgets for each field, including the contact’s name, phone number, and email. Finally, you have an Ok button to accept the dialog, and a Cancel button to dismiss the dialog.
  • Line 42 defines the .on_button_pressed() method. Textual will call this method whenever the user clicks a button on the screen.
  • Line 43 checks whether the clicked button is the Ok button. If that’s the case, then the code in lines 44 to 46 retrieves the data from each input widget. Finally, line 47 calls .dismiss() to return a tuple of the form (name, phone, email) from the dialog.
  • Line 49 runs when the user clicks the Cancel button, returning an empty tuple from the dialog.

It’s important to note that in this example, you haven’t done any input validation in the Add Contact dialog for the sake of simplicity. However, in a real-world app, you must always validate the user’s input to make sure the data is valid and secure to process.

With this code, the basic structure and functionality of your Add Contact dialog are all set up. However, you still need to add some CSS rules to improve the dialog’s appearance. Go back to the rpcontacts.tcss file and at the end, add the following code:

CSS rpcontacts/rpcontacts.tcss
/* ... */

InputDialog {
    align: center middle;
}

#title {
    column-span: 3;
    height: 1fr;
    width: 1fr;
    content-align: center middle;
    color: green;
    text-style: bold;
}

#input-dialog {
    grid-size: 3 5;
    grid-gutter: 1 1;
    padding: 0 1;
    width: 50;
    height: 20;
    border: solid green;
    background: $surface;
}

.label {
    height: 1fr;
    width: 1fr;
    content-align: right middle;
}

.input {
    column-span: 2;
}

These rules style different components of the Add Contact dialog. First, you center the dialog on the screen. Then, you style the title, the dialog frame, the labels, and the input widgets.

After these code additions, you’ll need to add some code that allows you to launch the Add Contact dialog from the app’s main TUI. And that’s exactly what you’ll do in the next section.

Launch and Process the Add Contact Dialog

Now that you’ve coded the Add Contact dialog and its associated CSS rules, it’s time to add a new method to ContactsApp so that you can launch the dialog by clicking on the Add button. This method must also allow you to process the user’s input once they click the dialog’s Ok button.

Go to the tui.py file and add the following code:

Python rpcontacts/tui.py
from textual.app import App, on
# ...

class ContactsApp(App):
    # ...

    @on(Button.Pressed, "#add")
    def action_add(self):
        def check_contact(contact_data):
            if contact_data:
                self.db.add_contact(contact_data)
                id, *contact = self.db.get_last_contact()
                self.query_one(DataTable).add_row(*contact, key=id)

        self.push_screen(InputDialog(), check_contact)

# ...

In this code snippet, you first import the @on decorator. You use this decorator to bind the Add button’s Pressed event to the .action_add() method. This way, when you click the Add button, the .action_add() will be called. Note that naming the method as an action will automatically bind it to the corresponding action on the app’s footer.

Inside .action_add(), you define an inner function that takes the response from the Add Contact dialog. If the response contains the data for a new contact, then you add the data to the database by calling .add_contact(). Next, you retrieve the contact’s ID and information from the database so that you can display the newly added contact in the list of contacts.

Note that the contact’s ID in the database must match the contact’s ID in the list of contacts. This will allow you to implement the contact deletion functionality in step 6.

Finally, you call .push_screen() with an instance of InputDialog and the inner function to launch the dialog and get its response, which will be processed by check_contact().

Now that you have a way to launch the Add Contact dialog and process its data, you can run the application again. Once the app is running, click the Add button to launch the dialog. Then, enter sample data for a new contact and accept the dialog by clicking Ok. The app should work like this:

Now, when you click the Add button, the Add Contact dialog appears on the screen. Once there, you can enter the contact’s data and click Ok to accept the dialog. This action will also add the data to the database and the list of contacts on the app’s TUI.

Step 6: Delete Contacts From the Database and TUI

The final feature you’ll add to your contact book application is the ability to remove one or all contacts from the database using the Delete and Clear All buttons on the app’s TUI.

Again, you’ll find all the files and code added or modified in this section under the source_code_step_6/ directory. You can download them by clicking the link below:

First, you’ll add the capability to delete one contact at a time. Then, you’ll add code to remove all the contacts from the database.

Delete the Selected Contact

To remove a contact, you select the target contact on the app’s TUI and press the Delete button. This action must remove the contact from the database and from the list of contacts on the TUI.

Go to the tui.py module and add the following code:

Python rpcontacts/tui.py
# ...

class ContactsApp(App):
    # ...

    @on(Button.Pressed, "#delete")
    def action_delete(self):
        contacts_list = self.query_one(DataTable)
        row_key, _ = contacts_list.coordinate_to_cell_key(
            contacts_list.cursor_coordinate
        )

        def check_answer(accepted):
            if accepted and row_key:
                self.db.delete_contact(id=row_key.value)
                contacts_list.remove_row(row_key)

        name = contacts_list.get_row(row_key)[0]
        self.push_screen(
            QuestionDialog(f"Do you want to delete {name}'s contact?"),
            check_answer,
        )

# ...

In this code snippet, you first bind the Delete button to the .action_delete() method using the @on decorator as you did before with the Add button.

Then, inside .action_delete(), you get a reference to the list of contacts on the TUI using the .query_one() method. After that, you get the row’s key, which uniquely identifies the selected row on the contacts list. This way, you can get the contact’s ID to delete the correct contact from the database.

When a user clicks the Delete button, you’d like to ask them to confirm that they want to remove the selected contact. To do this, you’ll reuse the Question dialog. To process the dialog’s response, you define the check_answer() inner function. If the user accepts the dialog, then you remove the contact from the database and the contacts list on the TUI.

The .push_screen() call launches the Question dialog with an appropriate message and lets you invoke check_answer() to process the response.

After these additions, you can run the application again to get the following behavior:

Now, when you select a contact from the list and click the Delete button, you’re presented with a confirmation message. If you accept the Question dialog by clicking Yes, then the application removes the selected contact from the database and updates the TUI accordingly.

Clear All the Contacts

To remove all the contacts from the database and from the app’s TUI, you’ll add a method called .action_clear_all() to ContactsApp:

Python rpcontacts/tui.py
# ...

class ContactsApp(App):
    # ...

    @on(Button.Pressed, "#clear")
    def action_clear_all(self):
        def check_answer(accepted):
            if accepted:
                self.db.clear_all_contacts()
                self.query_one(DataTable).clear()

        self.push_screen(
            QuestionDialog("Are you sure you want to remove all contacts?"),
            check_answer,
        )

# ...

Again, you use the @on decorator to bind the Clear All button to the .action_clear_all() method. In this method, you reuse the Question dialog again. To process the dialog’s response, you define the check_answer() function. If the user accepts the dialog, then the function removes all the contacts from the database and the app’s TUI.

To launch the dialog, you call the .push_screen() method with an instance of QuestionDialog and the inner function.

With this code in place, you can run the app again. It’ll behave like what’s shown below:

That’s it! With this last piece of code, your contact book application is complete. The application provides all the features your users will need to display, add, and remove contacts from the database.

Conclusion

Building a contact book application with Python, Textual, and SQLite can bolster your coding skills with these tools and overall as a developer. Coding a project like this allows you to apply the knowledge and skills you already have and also pushes you to learn and grow.

In this tutorial, you’ve learned how to:

  • Build the TUI for a contact book application using Textual
  • Handle the database operations using SQLite
  • Connect the app’s TUI to the database and make it functional

You can download the complete source code for the contact book application and also the code for each step in this tutorial by clicking the link below:

Next Steps

At this point, you’ve completed a fully functional contact book project. The application provides minimal functionality but it’s a good starting point. From here, you can continue to add features and take your Python and Textual skills to the next level. Here are some next step ideas that you can implement:

  • Add new data fields: You can add new data fields to store more information about your contacts. For example, you can add the contact’s picture, website information, and so on.
  • Provide search capability: You can give your users a way to search for a contact in the database, which is a really nice-to-have feature in this kind of application.
  • Add backup capability: You can provide a way to back up and export the contacts’ information.

These are just a few ideas for how you can continue to develop your contact book. Take the challenge and build something amazing on top of what you’ve already created!

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!