Developing a Habit Tracker

Developing positive habits and eliminating negative ones can be a challenging endeavor. We all strive to maintain consistency in our actions and achieve personal goals, but it’s not always easy to stay on track. Thankfully, the growing trend of habit trackers has emerged as a valuable tool to support individuals in their pursuit of self-improvement. With a quick visit to any popular app store, you’ll discover a vast array of habit-tracking applications available, catering to various needs and preferences. Nonetheless, in my quest to enhance my programming skills, I have decided to create my own habit-tracking application in Python as one of my first serious attempts at development.

Formally, a habit refers to a clearly defined task that requires periodic completion, such as daily teeth brushing or annual dentist visits. The fundamental components of a habit-tracking app can be outlined as follows:

  • The application allows users to define multiple habits, each with its own task specification and periodicity.
  • Users have the ability to mark tasks as completed, indicating that they have been checked off at any given time.
  • Each habit necessitates at least one task completion within the user-defined period. Failure to complete a habit during the specified period is considered breaking the habit.
  • If a user successfully completes a habit for a consecutive number of periods without any breaks, it is referred to as establishing a streak. For example, if a user works out every day for two full weeks, they would have a 14-day streak of exercising.
  • The app not only stores the habits entered by users but also provides analysis features. Users can obtain insights into various aspects, such as determining their longest habit streak or accessing a list of their current daily habits.

Conception

The most important part of any project is the conception phase because anything that is overlooked or forgotten in this phase has a negative effect on the implementation later and might lead to a failed project. So the first step of this project was to describe everything that was necessary to build the app and put it into a written concept. Following we will talk about the general idea for the program flow, the database schema, an overview of the different components, and how they interact with each other.

Program Flow

I decided to design my application similar to classic UNIX command line tools (e.g. ls, grep). This means, that the functionality of the application can be utilized directly from the command line by issuing parameters with the application call. So the application is started up, executes the given command, and terminates after every user interaction.

Data Storage

For persistent data storage, we will use Pythonโ€™s built-in sqlite3 library which allows us to setup a lightweight SQL database stored in a single file. This database file acts as the backbone of the application, with all functionality reading and writing from and to the database. The idea here is to embed SQL code, which allows precise manipulation and querying of the habit data, into the methods and functions written in Python. With this approach, we have full access to powerful SQL queries inside our application.

The data around a habit is stored in two different tables. The first table holds the data concerning a habit in general and the second table stores the tracking data of a habit, which means that every time a habit is checked-off, the current date is stored as an entry. Additionally, the database sports a periods table containing the supported periods (Daily, Weekly) and a user table for handling login/logout functionality.

 

Habit Management, Analytics, and CLI

We will store all the logic necessary for creating, deleting, and checking habits, in a class called Habit. The class consists of three methods, create, delete, and check that contain the respective SQL and Python logic. Upon success or failure of a command, a message is printed back to the command line.

For analyzing the habits we will create a separate module containing all necessary functions which will adhere to a functional programming paradigm to avoid side effects and changing the stored data. For implementing the control of the application with command line parameters, the Fire library is used. Fire utilizes a function that takes a module, object, class, or another function as an argument and exposes it to the command line.

To finally bring all the components together, the class Habit and the module containing the analytics functions are assigned to the class Pipeline. The Pipeline class effectively wraps all functionality, is then passed to the aforementioned function of the Fire module and an instance of the class is called in the main Python script.

 

Development

After discussing the concept that was used as a basis for development, we will now look at the actual implementation in greater detail. We will start off with an overview of the application structure and look into the individual components later on.

Application components

The application is split into 8 different files:

  • habits.py
    • Contains the class for creating, deleting, and checking habits and for exposing functionality to the command line.
  • analytics.py
    • Contains all functions for analyzing habits
  • pipeline.py
    • Contains a class for wrapping all functionality for the CLI
  • admin.py
    • Functions for initializing or deleting the database and generating random data entries
  • user.py
    • Functions for login, logout, and echoing the current user
  • tracker.py
    • The main file that exposes all functionality to the command line
  • decorators.py
    • Contains all decorators
  • test_tracker.py
    • Contains the unit test suite

tracker.py

The tracker.py file contains the main entry for regular program usage concerning management and analyzing habits. This is done by importing the Pipeline class which is then passed to the Fire library in the main loop. The main loop essentially consists of two checks, first if the database is initialized and second, if a user is logged in. After the successful completion of the two checks, Fire’s functionality is called with the user’s argument, effectively executing whatever action the user wants.

habits.py

The habits.py file contains the Habit class, which supports the three methods create, delete and check. The method names are as concise as possible to avoid complicated commands on the user side. When a new instance of the Habit class is created through Fire, the constructor establishes a database connection and assigns it as an attribute to the new instance. With this, the database connection is then available to all three methods. Additionally, the username of the actual user is also assigned as a class attribute.

Creating and Deleting a Habit

The create method takes the habits name and period as input from the user and a third argument entry_date which is at default today’s date. The date always defaults to the actual date with the __filter_date function if a user is logged in, so this argument is purely for the test data generator and the unit test to allow custom dates. The methods flow is first a call to the __filter_date function, second, a check if the period value is correct (Daily, Weekly), third a check if the habit does not already exist, and then an INSERT statement with the username and today’s date.

The delete method only takes the habit’s name as an argument. First, the method checks if the habit exists, followed by a DELETE statement.

Checking a Habit

The check method takes the habit’s name and date as an argument. As for the create method, the date is defaulted to today if a real user is logged in. The check method first checks if the given habit exists in the database, then evaluates if the habit is already checked for today. Then the method queries the given habits ID, followed by the period, and the last date on which the habit has been checked.

Following, the method evaluates if the habit is checked for the first time. If this is the first check, the streak is increased, else it must be evaluated if the streak is broken. If the period is daily, the last check date must be yesterday, else the streak is broken. If the period is weekly, the last check date must be at a maximum, seven days from today, or else the streak is broken. If the streak is broken the breaks must be incremented.

Then it must be checked if the current streak is also the longest streak. To evaluate this the method queries the LongestStreak entry and if the CurrentStreak is greater than the LongestStreak the value is increased. Lastly, a new entry in the tracking data table is inserted.

analytics.py

The analytics.py file contains the functions for analyzing habits and helper functions for establishing a database connection. The functions for normal program usage are combined in the class Analytics which is done solely for combining all functions in a single namespace. All functions are decorated with @staticmethod which allows calling the functions without initializing a class instance, keeping the function as pure as possible.

The functions were implemented with the help of a lot of list and dictionary comprehensions to avoid value assignments. The more complex functions are implemented by nesting functions and the usage of higher-order functions. Lastly, the functions are decorated with the @user_message decorator, which is a self-coded decorator that returns a message to the user if the function returns no data. With this decorator, we can avoid print statements in the functions.

pipeline.py

This file basically imports the Habit and Analytics classes and wraps the functionality of the two with the class Pipeline.

admin.py

The admin.pyย  file contains the functionality to initialize or reset the database, both for regular usage and by the unit test suite to set up a test environment. Additionally, we have a function for generating test data.

Creating and Deleting a Database

The initialize function establishes a database connection and then executes two CREATE TABLE statements. This creates the database.db file with the two tables habits and trackingdata. If a database already existed, the sqlite3 library returns an error, which is captured in an except block, and an error message is returned.

The second function, delete, first checks if a database file exists and then deletes it.

Generating Testdata

The third function testdata uses the Habit classes check method with randomly generated entry dates for a predefined dictionary filled with habits.

user.py

The user.py is used for all functions related to user management. The file contains a class User for wrapping the functions login, logout, and whoami, all decorated with @staticmethod. The first function, login takes a username as an argument and then writes it into the user table of the database.

The second function whoami is used to return the current user from the database. This function is also used by various other functions to get the actual username.

The third function logout, first checks if a user is logged in at the moment and then sets the user’s status to logged out in the user table.

decorators.py

The decoratos.py contains all decorators. The habit tracker contains two scenarios, where decorators are good alternatives to helper functions. First for the functions of the analytics.py and second for the unit test suite.ย  The first decorator @user_message is used to return a message to the user when an analytics function returns an empty dataset. Although not necessary, the user gets a simple message “No data” instead of an empty prompt if a function returns nothing. The core of the user_message decorator is thereby a conditional expression inside a try/except block. The called analytics function is checked with the any function. If the function returns an empty list the conditional expression evaluates to False and the message No Data is returned, else the functions return value is printed.

The second decorator @capture_print captures print statements for the unit test to evaluate. The function first checks if the username is testuser and then redirects the standard output to a variable. After capturing the value of the print statement in the variable, the standard output is then reset to normal.

test_tracker.py

The last component of the habit tracker is the unit test suite contained in test_tracker.py. The unit test suite was implemented with the unittest library included in Python. The test_tracker.py consists of two classes, TestHabit and TestAnalytics, which are then combined in one test suite. Both classes first construct a test environment in the form of a default database. After the set-up, the test for the various methods and functions is executed. After testing the functionality, the test database is deleted and the previous database, if it existed, is restored. Both classes make use of the various assert functions of the unittest library, to evaluate the results of queries or the return values of functions.

For testing the methods of Habit, data is first inserted, deleted, or checked via the respective methods and then the database entries are queried by a separate function. This query result is then compared against predefined data to ensure the methods work as expected. For testing the analytics functions, the test database is filled with predefined data, which is then analyzed. The return values are then compared against the expected values.

Conclusion

Looking back on the creation of the habit tracker, I can say that this project was a very pleasant and rewarding learning experience. The project definitely improved my programming skills by a
wide margin in regard to object-oriented and functional programming. Personally, I am satisfied with the final product’s functionality and the UNIX-tool-like control. The code for the habit tracker can be found in the following GitHub repo.