Building a Habit Tracker App with Streamlit and Python
Building tools to support personal development or project management often involves creating applications that can track progress over time. A habit tracker is a prime example, providing a visual and interactive way to monitor the consistency of activities. Developing such an application from scratch can be a complex undertaking, requiring expertise in frontend development, backend logic, and database management. However, using Python and the Streamlit library simplifies this process significantly, allowing for the creation of interactive web applications with minimal code and no need for separate frontend frameworks.
Streamlit is an open-source Python library specifically designed for machine learning and data science teams to create and share beautiful, custom web apps in minutes. Its strength lies in its simplicity and Python-native approach, enabling developers to build interactive interfaces directly from Python scripts. This makes it an excellent choice for prototyping and deploying simple, data-driven applications like a habit tracker.
Why Choose Streamlit for a Habit Tracker?
Developing a habit tracking application requires handling data input, storage, display, and potentially visualization. Traditional web development stacks involve multiple languages and frameworks. Streamlit streamlines this by:
- Rapid Prototyping: Turn data scripts into shareable web apps quickly. The iterative development cycle with Streamlit is very fast.
- Python Ecosystem: Leverage the extensive Python libraries for data handling (pandas), visualization (Matplotlib, Plotly, Altair), and file operations.
- No Frontend Expertise Required: Write the entire application logic and interface solely in Python. Streamlit handles the HTML, CSS, and JavaScript automatically.
- Interactivity: Easily add sliders, buttons, text inputs, checkboxes, and other widgets to build interactive elements.
- Data Flow: Streamlit’s data flow model simplifies state management for many common application patterns.
Compared to building a mobile app or using a complex web framework like Django or Flask with a separate frontend, Streamlit offers a path to a functional application much faster, especially for internal tools or personal projects.
Core Concepts for a Streamlit Habit Tracker
Understanding a few key concepts is essential before building the application:
- Streamlit Script Execution: A Streamlit app runs from top to bottom whenever a user interacts with a widget. State needs to be managed carefully.
- Widgets: Interactive elements like
st.button,st.checkbox,st.date_input,st.text_inputare used for user interaction. st.session_state: This dictionary-like object allows persistent state across user interactions within a single user’s session. It’s crucial for remembering data and application state.- Data Structure: How the habit data is organized. A common and efficient structure is a pandas DataFrame or a list of dictionaries, storing habit names, dates, and completion status.
- Data Persistence: Streamlit apps reset when the script re-runs or the server restarts. Saving data to a file (like CSV or JSON) or connecting to a database is necessary for long-term storage. For a simple app, saving to a local file is often sufficient.
- Visualization: Presenting habit consistency visually using charts or calendars can significantly improve the app’s usability.
Building the Habit Tracker: A Step-by-Step Guide
This guide outlines the process of creating a basic habit tracker using Streamlit and Python, focusing on core functionality: adding habits, tracking completion for a specific date, and saving/loading data.
Step 1: Project Setup
Begin by installing the necessary libraries. The primary library is streamlit, and pandas is highly recommended for efficient data handling.
pip install streamlit pandasCreate a Python file (e.g., habit_tracker_app.py) where the application code will reside.
Step 2: Data Structure Design
Design how the habit data will be stored. A pandas DataFrame is suitable, with columns to record essential information.
| Column Name | Data Type | Description |
|---|---|---|
Habit Name | string | The name of the habit (e.g., “Read for 30 mins”) |
Date | date | The date the habit was tracked for |
Completed | boolean | Whether the habit was completed on that date |
Initialize the DataFrame or load it from a persistent source. Using st.session_state to hold the DataFrame is key for maintaining data within a session without constantly re-loading from storage during interactions.
import streamlit as stimport pandas as pdfrom datetime import date
# Initialize session state for dataif 'habit_data' not in st.session_state: # Create an empty DataFrame if no data exists st.session_state['habit_data'] = pd.DataFrame(columns=['Habit Name', 'Date', 'Completed']) # Ensure 'Date' column is datetime type for easier handling st.session_state['habit_data']['Date'] = pd.to_datetime(st.session_state['habit_data']['Date'])
# Placeholder function to load data (implement file loading later)def load_data(): # In a real app, load from CSV/JSON/DB here # For this example, we rely on session_state unless implementing persistence pass # No loading needed if using only session_state
# Placeholder function to save data (implement file saving later)def save_data(): # In a real app, save to CSV/JSON/DB here st.session_state['habit_data'].to_csv('habit_data.csv', index=False) st.success("Data saved!")
# Load data when app starts (if implementing persistence)# load_data()Step 3: Add New Habits
Provide a way for users to define the habits they want to track. A text input and a button are suitable widgets.
st.title("Simple Habit Tracker")
# Section to add new habitsst.header("Add New Habit")new_habit_name = st.text_input("Enter habit name:")
if st.button("Add Habit"): if new_habit_name: # Add the new habit name to a list of unique habits # We need a separate list/set of unique habit names for display if 'unique_habits' not in st.session_state: st.session_state['unique_habits'] = []
if new_habit_name not in st.session_state['unique_habits']: st.session_state['unique_habits'].append(new_habit_name) st.success(f"Habit '{new_habit_name}' added!") else: st.warning(f"Habit '{new_habit_name}' already exists.") else: st.warning("Please enter a habit name.")Note: The unique habits list is stored separately from the tracking data for simpler display and selection later.
Step 4: Track Habits for a Specific Date
Allow users to select a date and mark habits as completed or not completed for that date. A date input and checkboxes are effective here.
st.header("Track Habits")
# Select date for trackingselected_date = st.date_input("Select a date:", date.today())
if 'unique_habits' in st.session_state and st.session_state['unique_habits']: st.subheader(f"Habits for {selected_date.strftime('%Y-%m-%d')}")
# Filter data for the selected date date_data = st.session_state['habit_data'][st.session_state['habit_data']['Date'].dt.date == selected_date]
updated_completions = {} for habit in st.session_state['unique_habits']: # Check if the habit is already recorded for this date habit_record = date_data[date_data['Habit Name'] == habit] initial_status = False if not habit_record.empty: initial_status = habit_record['Completed'].iloc[0]
# Use a unique key for each checkbox checkbox_key = f"checkbox_{habit}_{selected_date}" is_completed = st.checkbox(f"Completed: {habit}", value=initial_status, key=checkbox_key)
updated_completions[habit] = is_completed
# Button to save tracking changes for the selected date if st.button(f"Save Tracking for {selected_date.strftime('%Y-%m-%d')}"): # Prepare data to add/update records_to_add = [] # Remove existing records for this date to avoid duplicates before adding new ones st.session_state['habit_data'] = st.session_state['habit_data'][ (st.session_state['habit_data']['Date'].dt.date != selected_date) | # Keep records from other dates (~st.session_state['habit_data']['Habit Name'].isin(updated_completions.keys())) # Keep records for habits not being tracked on this date ].copy() # Use .copy() to avoid SettingWithCopyWarning
for habit, completed_status in updated_completions.items(): records_to_add.append({'Habit Name': habit, 'Date': selected_date, 'Completed': completed_status})
if records_to_add: # Append new records new_data = pd.DataFrame(records_to_add) new_data['Date'] = pd.to_datetime(new_data['Date']) # Ensure date is datetime st.session_state['habit_data'] = pd.concat([st.session_state['habit_data'], new_data], ignore_index=True)
st.success(f"Tracking updated for {selected_date.strftime('%Y-%m-%d')}")
else: st.info("Add some habits first in the 'Add New Habit' section.")This section displays checkboxes for each defined habit. The state of the checkbox is initialized based on existing data for the selected date. Clicking “Save Tracking” updates or adds records in the st.session_state['habit_data'] DataFrame.
Step 5: Display Tracking Data
Show the raw tracking data in a table for verification.
st.header("Tracking Data")st.dataframe(st.session_state['habit_data'])Step 6: Data Persistence (Saving and Loading)
Implement functions to save the habit_data DataFrame to a file and load it when the app starts. A simple CSV file is used here.
Modify the initial setup (Step 2) and add buttons for saving/loading.
# ... (previous imports and initial session_state setup) ...
DATA_FILE = 'habit_data.csv'UNIQUE_HABITS_FILE = 'unique_habits.json' # Using JSON for simplicity for unique habits
# Function to load data from CSVdef load_data(): try: df = pd.read_csv(DATA_FILE) df['Date'] = pd.to_datetime(df['Date']).dt.date # Store date as date object return df except FileNotFoundError: return pd.DataFrame(columns=['Habit Name', 'Date', 'Completed'])
# Function to save data to CSVdef save_data(df): df['Date'] = df['Date'].astype(str) # Convert date objects to string for saving df.to_csv(DATA_FILE, index=False)
import json# Function to load unique habits from JSONdef load_unique_habits(): try: with open(UNIQUE_HABITS_FILE, 'r') as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return []
# Function to save unique habits to JSONdef save_unique_habits(habits_list): with open(UNIQUE_HABITS_FILE, 'w') as f: json.dump(habits_list, f)
# Initialize session state OR load dataif 'habit_data' not in st.session_state: st.session_state['habit_data'] = load_data()
if 'unique_habits' not in st.session_state: st.session_state['unique_habits'] = load_unique_habits() # Also, initialize unique habits from loaded data if file was empty if not st.session_state['unique_habits'] and not st.session_state['habit_data'].empty: st.session_state['unique_habits'] = st.session_state['habit_data']['Habit Name'].unique().tolist()
st.title("Simple Habit Tracker")
# Add Save Buttonif st.button("Save All Data"): # Convert date column back to datetime objects before saving if they were stored as date objects # Or ensure consistent storage format. # For this example, we load as date.date and convert to string for saving CSV. # Let's refine date handling for consistency. Store as datetime objects in session state. st.session_state['habit_data']['Date'] = pd.to_datetime(st.session_state['habit_data']['Date']) # Ensure datetime in session state
save_data(st.session_state['habit_data']) save_unique_habits(st.session_state['unique_habits']) st.success("Data and habits saved!")
# ... (Add New Habit section - needs minor modification to save unique habits) ...st.header("Add New Habit")new_habit_name = st.text_input("Enter habit name:")
if st.button("Add Habit"): if new_habit_name: if 'unique_habits' not in st.session_state: st.session_state['unique_habits'] = []
if new_habit_name not in st.session_state['unique_habits']: st.session_state['unique_habits'].append(new_habit_name) save_unique_habits(st.session_state['unique_habits']) # Save unique habits immediately st.success(f"Habit '{new_habit_name}' added! Remember to Save All Data periodically.") else: st.warning(f"Habit '{new_habit_name}' already exists.") else: st.warning("Please enter a habit name.")
# ... (Track Habits section - no major changes needed, it operates on session_state['habit_data']) ...st.header("Track Habits")
# Select date for trackingselected_date = st.date_input("Select a date:", date.today())
if 'unique_habits' in st.session_state and st.session_state['unique_habits']: st.subheader(f"Habits for {selected_date.strftime('%Y-%m-%d')}")
# Ensure comparison with datetime objects if session state stores datetime date_data = st.session_state['habit_data'][st.session_state['habit_data']['Date'].dt.date == selected_date]
updated_completions = {} for habit in st.session_state['unique_habits']: habit_record = date_data[date_data['Habit Name'] == habit] initial_status = False if not habit_record.empty: initial_status = habit_record['Completed'].iloc[0]
checkbox_key = f"checkbox_{habit}_{selected_date}" is_completed = st.checkbox(f"Completed: {habit}", value=initial_status, key=checkbox_key)
updated_completions[habit] = is_completed
if st.button(f"Save Tracking for {selected_date.strftime('%Y-%m-%d')}"): # Convert selected_date to datetime for consistent comparison/storage selected_datetime = pd.to_datetime(selected_date)
# Remove existing records for this date and habit name st.session_state['habit_data'] = st.session_state['habit_data'][ (st.session_state['habit_data']['Date'].dt.date != selected_datetime.date()) | (~st.session_state['habit_data']['Habit Name'].isin(updated_completions.keys())) # This part is optional depending on desired behavior ].copy()
records_to_add = [] for habit, completed_status in updated_completions.items(): # Only add if completed (optional, could add False too) # Adding both True/False allows explicit tracking of non-completion records_to_add.append({'Habit Name': habit, 'Date': selected_datetime, 'Completed': completed_status})
if records_to_add: new_data = pd.DataFrame(records_to_add) st.session_state['habit_data'] = pd.concat([st.session_state['habit_data'], new_data], ignore_index=True) st.session_state['habit_data']['Date'] = pd.to_datetime(st.session_state['habit_data']['Date']) # Ensure datetime type after concat
st.success(f"Tracking updated for {selected_date.strftime('%Y-%m-%d')}")
else: st.info("Add some habits first in the 'Add New Habit' section.")
# ... (Tracking Data Display section - no changes) ...st.header("Tracking Data")st.dataframe(st.session_state['habit_data'])Step 7: Basic Visualization
Add a simple visualization to show progress, such as completion count per habit.
st.header("Habit Progress")
if not st.session_state['habit_data'].empty: # Calculate completion counts per habit completion_counts = st.session_state['habit_data'].groupby('Habit Name')['Completed'].sum().reset_index()
if not completion_counts.empty: st.subheader("Total Completions per Habit") # Use Streamlit's built-in bar chart st.bar_chart(completion_counts.set_index('Habit Name'))
# Optional: Show completion rate over time for a selected habit if 'unique_habits' in st.session_state and st.session_state['unique_habits']: st.subheader("Completion Rate Over Time") selected_habit_viz = st.selectbox("Select a habit to visualize:", st.session_state['unique_habits'])
if selected_habit_viz: habit_data_filtered = st.session_state['habit_data'][st.session_state['habit_data']['Habit Name'] == selected_habit_viz].copy()
if not habit_data_filtered.empty: # Sort by date habit_data_filtered = habit_data_filtered.sort_values(by='Date')
# Create a time series of completion status (1 for True, 0 for False) habit_data_filtered['Completed_Value'] = habit_data_filtered['Completed'].astype(int)
st.line_chart(habit_data_filtered.set_index('Date')['Completed_Value']) else: st.info("No data tracked yet for this habit.")else: st.info("Track some habits to see progress.")This adds a bar chart showing the total number of times each habit has been marked as completed and a line chart showing completion status over time for a selected habit. Streamlit’s built-in charting functions (st.bar_chart, st.line_chart) are convenient for quick visualizations using pandas DataFrames.
Step 8: Running the App
Save the Python file (e.g., habit_tracker_app.py). Open a terminal or command prompt, navigate to the directory where the file is saved, and run the command:
streamlit run habit_tracker_app.pyThis command starts a local web server, and your habit tracker application will open in a web browser.
Practical Application Example
Consider an individual aiming to improve consistency in several areas: daily coding practice, reading a chapter of a book, and taking a 30-minute walk. They could use the Streamlit habit tracker to:
- Add Habits: Input “Coding Practice”, “Read Book”, “Walk 30 mins” into the “Add New Habit” section.
- Track Daily: Each evening, they open the app, select the current date, and check the boxes for the habits they completed.
- Monitor Progress: Use the “Habit Progress” section to see how many times they’ve completed each habit in total and observe the trend over time using the line chart.
- Persistence: Click “Save All Data” periodically to ensure their tracking history is stored in the
habit_data.csvfile.
This demonstrates how the simple Streamlit app provides a functional, visual tool for self-monitoring without the overhead of more complex development environments. The flexibility of Python and Streamlit allows for future expansion, such as adding features like streak counting, goal setting, or more sophisticated visualizations.
Summary: Key Takeaways for Building a Habit Tracker with Streamlit
- Streamlit enables rapid development of interactive web applications using only Python.
st.session_stateis essential for managing data and application state across user interactions within a session.- A pandas DataFrame provides a flexible structure for storing habit tracking data (
Habit Name,Date,Completed). - Data persistence requires saving the DataFrame to a file (like CSV) or a database and loading it on application start.
- Streamlit widgets (
st.text_input,st.button,st.date_input,st.checkbox) are used to build the user interface for adding habits and tracking completion. - Basic visualizations can be easily added using Streamlit’s built-in charting functions or by integrating libraries like Matplotlib, Plotly, or Altair.
- Running the app is straightforward using the
streamlit runcommand. - The approach is highly suitable for personal tools, internal dashboards, or prototypes where rapid development and Python integration are priorities.