Create a Simple Python CLI for Managing Tasks
Step-by-step guide to creating a simple CLI tool in Python

I am a Frontend developer transitioning from IT Administration. Passionate about creating seamless user experiences and continuously improving my skills, I aim to become one of the best in frontend development. Let's connect and grow together!
This is a simple Python application for managing tasks. The project introduces basic concepts that are essential building blocks for any application. So, open your favorite code editor, and let's get started.
Main Menu
Let's start simple by defining the main function, which will be the starting point of the application. Create a dictionary with menu options, then loop through the items to display the values along with their keys.:
def main():
"""Main function"""
menu = {
'1': 'Add Task',
'2': 'List Tasks',
'3': 'Complete Task',
'4': 'Delete Task',
'0': 'Exit'
}
print('\n=== Task Manager ===')
for key, value in menu.items():
print(f"{key}. {value}")
print('----------------------')
To test the output, let’s make sure the current script is run directly and not as an imported module:
...
if __name__ == "__main__":
main()
In your terminal, run the script:
$ python3 app.py
=== Task Manager ===
1. Add Task
2. List Tasks
3. Complete Task
4. Delete Task
0. Exit
----------------------
The options are displayed in a clear format. Great! Now, let's outline the different parts we need to work on to make this application functional:
Ask the user to choose an option and record their input.
Define functions to handle each user option.
Gracefully handle unexpected user entries and implement error checking.
Get User Input
Prompt the user to select an option from among those listed:
choice = input("\nSelect an option: ").strip()
Please note that any input entered will be stored as a string. So, when the user enters 1, for example, it will be captured as "1". The strip function removes any whitespace that the user may have entered accidentally or intentionally..
Let’s now check what choice the user picks. We will use an if…elif…else block to make sure the choice picked matches the menu item:
if choice == '1':
print("=== Add Task ===")
elif choice == '2':
print("=== List Tasks ===")
elif choice == '3':
print('=== Complete Task ===\n')
elif choice == '4':
print('=== Delete Task ===')
elif choice == '0':
print('Exiting Task Manager...')
print('Done')
else:
print('Invalid option.\n')
To improve the interactivity of this application, we wrap this logic in a while loop which will keep the program running until the user exits:
def main():
"""Main function"""
menu = {
'1': 'Add Task',
'2': 'List Tasks',
'3': 'Complete Task',
'4': 'Delete Task',
'0': 'Exit'
}
while True:
print('\n=== Task Manager ===')
for key, value in menu.items():
print(f"{key}. {value}")
print('----------------------')
choice = input("\nSelect an option: ").strip()
if choice == '1':
print("=== Add Task ===")
elif choice == '2':
print("=== List Tasks ===")
elif choice == '3':
print('=== Complete Task ===\n')
elif choice == '4':
print('=== Delete Task ===')
elif choice == '0':
message = 'Exiting Task Manager...'
print('Done!')
break
else:
print('Invalid option.\n')
if __name__ == "__main__":
main()
Until the user exits (by selecting "0"), the program will continue to prompt for input. This break mechanism offers an easy way to exit the program. If the user chooses an option not listed, a message will appear to inform them that the choice is invalid.
Define Methods for Handling Tasks
We will utilize classes to manage the tasks created by users and define various methods for manipulating these tasks. Using classes gives more structure, encapsulation, and flexibility as the app grows. We will create two classes: TaskManager to store tasks by ID and Task to define the structure of a task.
Task Class
from datetime import datetime
# Define a Task class
class Task:
def __init__(self, task_id, title, completed=False, date_added=None, date_completed=None):
self.task_id = task_id
self.title = title
self.completed = completed
self.date_added = date_added or datetime.now()
self.date_completed = date_completed
The __init__ method is a special built-in Python method, also called a constructor, that is automatically called when an object is created from a class (in this case, Task). It sets up the initial state of the object. When a new task is created, task_id and title must be provided. The default attributes completed, date_added, and date_completed are automatically set when creating a new task. We need to import the datetime package to get the creation date.
Next, let’s define a method to display the representation of the task. We will use the built-in repr() function.
# Define a Task class
class Task:
def __init__(self, task_id, title, completed=False, date_added=None, date_completed=None):
self.task_id = task_id
self.title = title
self.completed = completed
self.date_added = date_added or datetime.now()
self.date_completed = date_completed
def __repr__(self):
status = "[x]" if self.completed else "[ ]"
date_info = f"Added: {self.date_added.strftime('%Y-%m-%d')}"
if self.completed:
date_info += f" | Completed: {self.date_completed.strftime('%Y-%m-%d')}"
return f"{status} {self.task_id}: {self.title} [{date_info}]"
In the __repr__() function, we determine the status of the task, showing [x] if it is completed, otherwise [ ]. Next, we create a date_info variable to show the date the task was created in the format YYYY-MM-DD. If the task has been marked as completed, we will also include the completed date in the same format.
TaskManager Class
# Define a TaskManager to store tasks by ID
class TaskManager:
def __init__(self):
self.tasks = {}
self.task_id = 1 # to auto-increment
We start by creating an empty dictionary to store the tasks and a task_id attribute to identify each task uniquely.
Add Task
Let’s include the functionality to add tasks in the TaskManager class:
class TaskManager:
def __init__(self):
self.tasks = {}
self.task_id = 1
def add_task(self, title):
"""Add new task"""
new_task = Task(self.task_id, title.capitalize())
self.tasks[self.task_id] = new_task
self.task_id += 1
return {'success': f"Task '{title.capitalize()}' added."}
The add_task method accepts a task title and creates a new task object from the Task class. It then adds this new task object to the tasks dictionary, using the task_id identifier as the key. After that, it increments the identifier for the next created task and returns a dictionary that will be used to check the status of the operation.
Back in the main function, when the user selects the option to create a new task, invoke this add_task method:
def main():
"""Main function"""
tm = TaskManager() # Create an instance of the task manager class
menu = {
'1': 'Add Task',
'2': 'List Tasks',
'3': 'Complete Task',
'4': 'Delete Task',
'0': 'Exit'
}
while True:
print('\n=== Task Manager ===')
for key, value in menu.items():
print(f"{key}. {value}")
print('----------------------')
choice = input("\nSelect an option: ").strip()
if choice == '1':
print("=== Add Task ===")
title = input("Enter task title: ").strip()
status = tm.add_task(title)
print(status['success'])
The main function starts by creating an instance of the TaskManager class, giving us access to its methods. When the user chooses to add a task, they are asked to enter the task title. This trimmed title is then passed to the add_task method. The resulting status dictionary is stored in the status variable and displayed on the console.
# Sample output
=== Task Manager ===
1. Add Task
2. List Tasks
3. Complete Task
4. Delete Task
0. Exit
----------------------
Select an option: 1
=== Add Task ===
Enter task title: study python
Task 'Study python' added.
List Tasks
Include the list_tasks method in TaskManager:
class TaskManager:
def __init__(self):...
def add_task(self, title):...
def list_tasks(self):
"""List tasks"""
if len(self.tasks):
print("Status | ID | Title | Date Added | Date Completed |")
print("---------------------------------------------------------------------------")
for task in self.tasks.values():
status = "[x]" if task.completed else "[ ]"
add_date = task.date_added.strftime('%Y-%m-%d')
complete_date = task.date_completed.strftime('%Y-%m-%d') if task.date_completed else 'Pending'
print(f"{status:^6} | {task.task_id:<2} | {task.title:<30} | {add_date} | {complete_date:<14} |")
else:
print("No saved tasks.")
Let’s break down what happens here:
The
list_tasksmethod checks if there are any tasks in thetasksdictionary. If there are no tasks, the user is informed.The heading is formatted and printed with the right spacing and dividers.
We go through each task in the
tasksdictionary to process them.Inside the loop, the formatting for
status(completed or not),add_dateandcomplete_dateis set.The task details are printed with the right spacing.
# Sample output
=== List Tasks ===
Status | ID | Title | Date Added | Date Completed |
----------------------------------------------------------------------------
[ ] | 1 | Study python | 2025-05-14 | Pending |
[ ] | 2 | Finish up portfolio project | 2025-05-14 | Pending |
[x] | 3 | Exercise | 2025-05-14 | 2025-05-14 |
Two tasks are still pending. One is completed.
Complete Task
To do this, we need to update the completed and date_completed attributes for each task. We will add a method called complete_task in the Task class to access and modify these attributes:
class Task:
def __init__(self, task_id, title, completed=False, date_added=None, date_completed=None):
self.task_id = task_id
self.title = title
self.completed = completed
self.date_added = date_added or datetime.now()
self.date_completed = date_completed
def complete_task(self):
"""Mark task as completed and set completion time and date"""
self.completed = True
self.date_completed = datetime.now()
def __repr__(self):
status = "[x]" if self.completed else "[ ]"
date_info = f"Added: {self.date_added.strftime('%Y-%m-%d')}"
if self.completed:
date_info += f" | Completed: {self.date_completed.strftime('%Y-%m-%d')}"
return f"{status} {self.task_id}: {self.title} [{date_info}]"
This method sets the completed state to True and updates date_completed to the current date and time.
In the TaskManager class, add a method named mark_completed that will accept the task ID and call the complete_task method for that specific task:
class TaskManager:
def __init__(self):...
def add_task(self, title):...
def list_tasks(self):...
def mark_completed(self, task_id):
"""Mark task as completed"""
self.tasks[task_id].complete_task()
return {'success': f"Task {task_id} marked as completed."}
After marking the task as completed, the method returns a dictionary indicating the status. Back in the main function, we manage the user's choice to complete a task:
def main():
"""Main function"""
tm = TaskManager()
menu = {...}
while True:
print('\n=== Task Manager ===')
for key, value in menu.items():
print(f"{key}. {value}")
print('----------------------')
choice = input("\nSelect an option: ").strip()
if choice == '1':...
elif choice == '2':...
elif choice == '3':
print('=== Complete Task ===\n')
print('Available tasks')
print('---------------\n')
tm.list_tasks()
task_id = input("\nEnter task ID: ").strip()
status = tm.mark_completed(int(task_id))
print(status['success'])
The user sees a list of available tasks and is asked to enter the ID of the task they want to complete. The ID is converted to an integer and sent to the mark_completed method. The status dictionary returned is saved in the status variable and shown on the console.
# Sample output
=== Complete Task ===
Available tasks
---------------
Status | ID | Title | Date Added | Date Completed |
----------------------------------------------------------------------------
[ ] | 1 | Exercise | 2025-05-14 | Pending |
Enter task ID: 1
Task 1 marked as completed.
# List tasks to confirm task completion
=== List Tasks ===
Status | ID | Title | Date Added | Date Completed |
----------------------------------------------------------------------------
[x] | 1 | Exercise | 2025-05-14 | 2025-05-14 |
Delete Task
Finally, we manage the user's choice to delete a task. Add the delete_task method to the TaskManager class. This method will delete the task linked to the task_id and return a dictionary indicating whether the deletion was successful:
class TaskManager:
def __init__(self):...
def add_task(self, title):...
def list_tasks(self):...
def mark_completed(self, task_id):...
def delete_task(self, task_id):
del self.tasks[task_id]
return {'success': f"Task {task_id} deleted."}
Let’s invoke this method in the main function:
def main():
"""Main function"""
tm = TaskManager()
menu = {...}
while True:
print('\n=== Task Manager ===')
for key, value in menu.items():
print(f"{key}. {value}")
print('----------------------')
choice = input("\nSelect an option: ").strip()
if choice == '1':...
elif choice == '2':...
elif choice == '3':...
elif choice == '4':
print('=== Delete Task ===')
print('Available tasks')
print('---------------\n')
tm.list_tasks()
task_id = input("\nEnter task ID: ").strip()
status = tm.delete_task(int(task_id))
print(status['success'])
# Sample output
...
=== Add Task ===
Enter task title: exercise
Task 'Exercise' added.
...
=== Delete Task ===
Available tasks
---------------
Status | ID | Title | Date Added | Date Completed |
----------------------------------------------------------------------------
[ ] | 1 | Exercise | 2025-05-14 | Pending |
Enter task ID: 1
Task 1 deleted.
=== Task Manager ===
1. Add Task
2. List Tasks
3. Complete Task
4. Delete Task
0. Exit
----------------------
Select an option: 2
=== List Tasks ===
No saved tasks.
Additional Features and Error Handling
The basic functionality of this program is complete. However, we can make it more robust by handling errors and enhancing some of its features. A few of the features we’ll include:
Data validation
Check for duplicate entries to ensure no two tasks have the same title.
Confirm the existence of the task to be completed or deleted.
Before completing a task, make sure it hasn't already been completed.
Clear the screen when navigating different menu options to create more space on the console.
Feature 1 - Data validation
Before processing the data, let's ensure it is in the correct format. For each prompt, we will also include the possibility of an early exit.
Add task:
if choice == '1': print("=== Add Task ===") while True: title = input("Enter task title ('q' to exit): ").strip() if not title: print("Please enter a task title or 'q' to exit.") continue if title.lower() == 'q': print("Cancelled adding task.") break if len(title) < 3: print("Title length should be at least 3 characters.") continue status = tm.add_task(title) if 'error' in status: print(f"Error: {status['error']}") continue print(status['success']) breakWrap the logic in a
whileloop so the user is continuously prompted for correct input. If the user pressesEnterwithout typing a title, or if the title is shorter than 3 characters, inform them of the issue and allow them to try again. Thecontinuekeyword returns the program to the start of the while loop. If the user types lowercaseq, the program exits the submenu.Notice the order of the checks. If the
len(title) < 3:check came beforetitle.lower() == 'q':, it would be impossible to exit the submenu becauseqis always less than 3 characters and would be ignored.If an error status is returned from the task manager, the error message is displayed in the console, and the user is asked to enter a task title again. We will address duplicate entry checking in Feature 2.
# Sample output === Add Task === Enter task title ('q' to exit): # No title supplied Please enter a task title or 'q' to exit. Enter task title ('q' to exit): ae # Title less that 3 characters Title length should be at least 3 characters. Enter task title ('q' to exit): q # Quit condition Cancelled adding task.Complete task
elif choice == '3': print('=== Complete Task ===\n') print('Available tasks') print('---------------\n') tm.list_tasks() while True: task_id = input("\nEnter task ID ('q' to exit): ").strip() if not task_id: print("Please enter a task ID or 'q' to exit.") continue if task_id.lower() == 'q': print("Cancelled completing task.") break if not task_id.isdigit(): print('Task ID should be a number.') continue status = tm.mark_completed(int(task_id)) if 'error' in status: print(f"Error: {status['error']}") continue print(status['success']) breakRemember that data is always captured as a string by the
input()function. The third check makes sure the data can be converted into an integer. For example, if the user enters ‘x’ as the task ID,status = tm.mark_completed(int(task_id))would cause an error because ‘x’ cannot be turned into an integer, but ‘4’ can. The rest of the process is similar to adding a task.Delete task
elif choice == '4': print('=== Delete Task ===') print('Available tasks') print('---------------\n') tm.list_tasks() while True: task_id = input("\nEnter task ID ('q' to exit): ").strip() if not task_id: print("Please enter a task ID or 'q' to exit.") continue if task_id.lower() == 'q': print("Cancelled deleting task.") break if not task_id.isdigit(): print('Task ID should be a number.') continue status = tm.delete_task(int(task_id)) if 'error' in status: print(f"Error: {status['error']}") continue print(status['success']) break
Feature 2 - Check duplicate entries
When adding a new task, we want to ensure there isn't already one with the same title.
class TaskManager:
def __init__(self):...
def add_task(self, title):
# Loop through the saved tasks and compare each
# task title with the one supplied by the user
if any(title.lower() == task.title.lower() for task in self.tasks.values()):
# Return an error status if duplicate found
return {'error': "A task with this title already exists."}
new_task = Task(self.task_id, title.capitalize())
self.tasks[self.task_id] = new_task
self.task_id += 1
return {'success': f"Task '{title.capitalize()}' added."}
# Sample output
...
=== List Tasks ===
Status | ID | Title | Date Added | Date Completed |
----------------------------------------------------------------------------
[ ] | 1 | Exercise | 2025-05-14 | Pending |
...
=== Add Task ===
Enter task title ('q' to exit): EXERCISE
Error: A task with this title already exists.
Enter task title ('q' to exit):
Feature 3 - Confirm task exists before completing or deleting
Notify user user if the task does not exist:
def mark_completed(self, task_id):
if task_id in self.tasks: # Check if task exists before completing
self.tasks[task_id].complete_task()
return {'success': f"Task {task_id} marked as completed."}
return {'error': f"Task with id {task_id} not found."}
def delete_task(self, task_id):
if task_id in self.tasks: # Confirm task exists before attempting deletion
del self.tasks[task_id]
return {'success': f"Task {task_id} deleted."}
return {'error': f"Task with id {task_id} not found."}
# Sample output
=== Delete Task ===
Available tasks
---------------
Status | ID | Title | Date Added | Date Completed |
----------------------------------------------------------------------------
[ ] | 1 | Pracite piano | 2025-05-15 | Pending |
Enter task ID ('q' to exit): 2
Error: Task with id 2 not found.
Feature 4 - Confirm task is not completed before completing
def mark_completed(self, task_id):
if task_id in self.tasks: # Confirm task first exists
if self.tasks[task_id].completed: # Confirm if already completed
return {'error': f"Task with ID {task_id} already marked as completed"}
self.tasks[task_id].complete_task()
return {'success': f"Task {task_id} marked as completed."}
return {'error': f"Task with id {task_id} not found."}
# Sample output
=== Complete Task ===
Available tasks
---------------
Status | ID | Title | Date Added | Date Completed |
----------------------------------------------------------------------------
[x] | 1 | Pracite piano | 2025-05-15 | 2025-05-16 |
Enter task ID ('q' to exit): 1
Error: Task with ID 1 already marked as completed
Feature 5 - Clear screen
At the top of the file import os module. We need this module to clear the console.
from datetime import datetime
import os # <= This one
class Task:...
class TaskManager:...
def clear_screen():
"""Clear the console"""
os.system('cls' if os.name == 'nt' else 'clear')
The clear_screen function checks if the operating system is Windows-based ('nt') to decide whether to use 'cls' (for Windows) or 'clear' for other systems. This ensures the program can run on any platform.
We will use this function after the user selects a menu option:
def main():
tm = TaskManager()
menu = {...}
while True:
...
choice = input("\nSelect an option: ").strip()
clear_screen() # Clears screen after menu choice
Future Improvement Considerations
The program works well for managing user tasks. However, there are many opportunities for improvement. Here are a few ideas you can consider:
Split into modules - divide the code into separate files, such as
task.pyfor theTaskclass andtask_manager.pyfor theTaskManager. This approach aids in scalability and testing.Confirm destructive actions - before deleting, ask the user to confirm if they are sure.
Sort and filter tasks - let users view only complete/incomplete tasks, or sort by date added to title
Add persistence - use JSON to save tasks between sessions
Add features - search tasks, include task categories or priorities, let users set due dates and reminders, highlight overdue tasks
Testing - start writing unit tests with
unittestorpytest. This builds reliability as your code grows
You can also stretch your goals by building a:
GUI versionusingtkinterorPyQtWeb appwith Flask or DjangoTUI versionwithrichortextual
Here is the full program code:
from datetime import datetime
import os
# Define a Task class
class Task:
def __init__(self, task_id, title, completed=False, date_added=None, date_completed=None):
self.task_id = task_id
self.title = title
self.completed = completed
self.date_added = date_added or datetime.now()
self.date_completed = date_completed
def complete_task(self):
"""Mark task as completed and set completion time and date"""
self.completed = True
self.date_completed = datetime.now()
def __repr__(self):
status = "[x]" if self.completed else "[ ]"
date_info = f"Added: {self.date_added.strftime('%Y-%m-%d')}"
if self.completed:
date_info += f" | Completed: {self.date_completed.strftime('%Y-%m-%d')}"
return f"{status} {self.task_id}: {self.title} [{date_info}]"
# Define a TaskManager to store tasks by ID
class TaskManager:
def __init__(self):
self.tasks = {}
self.task_id = 1 # to auto-increment
def add_task(self, title):
if any(title.lower() == task.title.lower() for task in self.tasks.values()):
return {'error': "A task with this title already exists."}
new_task = Task(self.task_id, title.capitalize())
self.tasks[self.task_id] = new_task
self.task_id += 1
return {'success': f"Task '{title.capitalize()}' added."}
def list_tasks(self):
if len(self.tasks):
print("Status | ID | Title | Date Added | Date Completed |")
print("----------------------------------------------------------------------------")
for task in self.tasks.values():
status = "[x]" if task.completed else "[ ]"
add_date = task.date_added.strftime('%Y-%m-%d')
complete_date = task.date_completed.strftime('%Y-%m-%d') if task.date_completed else 'Pending'
print(f"{status:^6} | {task.task_id:<2} | {task.title:<30} | {add_date} | {complete_date:<14} |")
else:
print("No saved tasks.")
def mark_completed(self, task_id):
if task_id in self.tasks:
if self.tasks[task_id].completed:
return {'error': f"Task with ID {task_id} already marked as completed"}
self.tasks[task_id].complete_task()
return {'success': f"Task {task_id} marked as completed."}
return {'error': f"Task with id {task_id} not found."}
def delete_task(self, task_id):
if task_id in self.tasks:
del self.tasks[task_id]
return {'success': f"Task {task_id} deleted."}
return {'error': f"Task with id {task_id} not found."}
def clear_screen():
os.system('cls' if os.name == 'nt' else 'clear')
def main():
tm = TaskManager()
menu = {
'1': 'Add Task',
'2': 'List Tasks',
'3': 'Complete Task',
'4': 'Delete Task',
'0': 'Exit'
}
while True:
print('\n=== Task Manager ===')
for key, value in menu.items():
print(f"{key}. {value}")
print('----------------------')
choice = input("\nSelect an option: ").strip()
clear_screen()
if choice == '1':
print("=== Add Task ===")
while True:
title = input("Enter task title ('q' to exit): ").strip()
if not title:
print("Please enter a task title or 'q' to exit.")
continue
if title.lower() == 'q':
print("Cancelled adding task.")
break
if len(title) < 3:
print("Title length should be at least 3 characters.")
continue
status = tm.add_task(title)
if 'error' in status:
print(f"Error: {status['error']}")
continue
print(status['success'])
break
elif choice == '2':
print("=== List Tasks ===")
tm.list_tasks()
elif choice == '3':
print('=== Complete Task ===\n')
print('Available tasks')
print('---------------\n')
tm.list_tasks()
while True:
task_id = input("\nEnter task ID ('q' to exit): ").strip()
if not task_id:
print("Please enter a task ID or 'q' to exit.")
continue
if task_id.lower() == 'q':
print("Cancelled completing task.")
break
if not task_id.isdigit():
print('Task ID should be a number.')
continue
status = tm.mark_completed(int(task_id))
if 'error' in status:
print(f"Error: {status['error']}")
continue
print(status['success'])
break
elif choice == '4':
print('=== Delete Task ===')
print('Available tasks')
print('---------------\n')
tm.list_tasks()
while True:
task_id = input("\nEnter task ID ('q' to exit): ").strip()
if not task_id:
print("Please enter a task ID or 'q' to exit.")
continue
if task_id.lower() == 'q':
print("Cancelled deleting task.")
break
if not task_id.isdigit():
print('Task ID should be a number.')
continue
status = tm.delete_task(int(task_id))
if 'error' in status:
print(f"Error: {status['error']}")
continue
print(status['success'])
break
elif choice == '0':
print('Exiting Task Manager...')
print('\nDone!')
break
else:
print('Invalid option.\n')
if __name__ == "__main__":
main()



