Editor

To-Do List in Python

Beginner · Useful tools · about 45 minutes to build · 109 lines

Add tasks, tick them off as you finish them and find them again next time, because the list is saved in a text file.

The program

Press Run and answer its questions in the box that appears under the code.

to_do_list.py
"""A to-do list that saves your tasks in a text file."""
FILE_NAME = "todo.txt"
DONE_MARK = "[x] "
OPEN_MARK = "[ ] "
MENU = "1) Add 2) List 3) Mark done 4) Delete 5) Quit"
# Saving and loading
def load_tasks(file_name):
"""Read the saved tasks, or return an empty list if there is no file yet."""
tasks = []
try:
with open(file_name, encoding="utf-8") as file:
for line in file:
line = line.rstrip("\n")
if line.startswith(DONE_MARK):
tasks.append({"text": line[len(DONE_MARK):], "done": True})
elif line.startswith(OPEN_MARK):
tasks.append({"text": line[len(OPEN_MARK):], "done": False})
elif line.strip():
# A line typed into the file by hand counts as not done
tasks.append({"text": line.strip(), "done": False})
except FileNotFoundError:
pass # First run: nothing has been saved yet
return tasks
def save_tasks(file_name, tasks):
"""Write every task to the file, one per line."""
with open(file_name, "w", encoding="utf-8") as file:
for task in tasks:
mark = DONE_MARK if task["done"] else OPEN_MARK
file.write(mark + task["text"] + "\n")
# Showing and choosing tasks
def show_tasks(tasks):
"""Print the numbered list and how many tasks are done."""
if not tasks:
print(" Your list is empty.")
return
for number, task in enumerate(tasks, start=1):
mark = DONE_MARK if task["done"] else OPEN_MARK
print(f" {number:>2}. {mark}{task['text']}")
done = sum(1 for task in tasks if task["done"])
print(f" {done} of {len(tasks)} done")
def choose_task(tasks, action):
"""Ask for a task number and return its index, or None to cancel."""
if not tasks:
print(" There are no tasks yet.")
return None
show_tasks(tasks)
while True:
text = input(f"Task number to {action} (Enter to cancel): ").strip()
if text == "":
return None
if text.isdecimal() and 1 <= int(text) <= len(tasks):
return int(text) - 1
print(f" Please type a number from 1 to {len(tasks)}.")
# The menu
def main():
"""Load the list, run the menu, and save after every change."""
tasks = load_tasks(FILE_NAME)
print(f"To-do list: {len(tasks)} task(s) loaded from {FILE_NAME}")
while True:
print()
print(MENU)
choice = input("Choose 1-5: ").strip()
if choice == "1":
text = input("New task: ").strip()
if text:
tasks.append({"text": text, "done": False})
save_tasks(FILE_NAME, tasks)
print(f" Added: {text}")
else:
print(" A task needs some words. Nothing added.")
elif choice == "2":
show_tasks(tasks)
elif choice == "3":
index = choose_task(tasks, "mark done")
if index is not None and tasks[index]["done"]:
print(" That task is already done.")
elif index is not None:
tasks[index]["done"] = True
save_tasks(FILE_NAME, tasks)
print(f" Done: {tasks[index]['text']}")
elif choice == "4":
index = choose_task(tasks, "delete")
if index is not None:
removed = tasks.pop(index)
save_tasks(FILE_NAME, tasks)
print(f" Deleted: {removed['text']}")
elif choice == "5":
print(f"Your tasks are saved in {FILE_NAME}. Goodbye.")
break
else:
print(" Please choose a number from 1 to 5.")
if __name__ == "__main__":
main()

Open it in the full editor to save it, share it and keep building.

Sample run

What the program printed, and what was typed (in yellow).

To-do list: 0 task(s) loaded from todo.txt

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 1
New task: Finish maths homework
  Added: Finish maths homework

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 1
New task: Buy milk
  Added: Buy milk

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 1
New task: 
  A task needs some words. Nothing added.

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 1
New task: Call grandma
  Added: Call grandma

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 3
   1. [ ] Finish maths homework
   2. [ ] Buy milk
   3. [ ] Call grandma
  0 of 3 done
Task number to mark done (Enter to cancel): 2
  Done: Buy milk

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 4
   1. [ ] Finish maths homework
   2. [x] Buy milk
   3. [ ] Call grandma
  1 of 3 done
Task number to delete (Enter to cancel): seven
  Please type a number from 1 to 3.
Task number to delete (Enter to cancel): 3
  Deleted: Call grandma

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 2
   1. [ ] Finish maths homework
   2. [x] Buy milk
  1 of 2 done

1) Add  2) List  3) Mark done  4) Delete  5) Quit
Choose 1-5: 5
Your tasks are saved in todo.txt. Goodbye.

How it is built

1. How tasks are stored

Each task is one line in todo.txt, starting with [x] when it is done and [ ] when it is not. The file stays readable in any text editor.

FILE_NAME = "todo.txt"
DONE_MARK = "[x] "
OPEN_MARK = "[ ] "
MENU = "1) Add  2) List  3) Mark done  4) Delete  5) Quit"

2. Loading, even on the first run

On the first run there is no file, so open() raises FileNotFoundError and the function returns an empty list. Each line becomes a dictionary with text and done.

def load_tasks(file_name):
    """Read the saved tasks, or return an empty list if there is no file yet."""
    tasks = []
    try:
        with open(file_name, encoding="utf-8") as file:
            for line in file:
                line = line.rstrip("\n")
                if line.startswith(DONE_MARK):
                    tasks.append({"text": line[len(DONE_MARK):], "done": True})
                elif line.startswith(OPEN_MARK):
                    tasks.append({"text": line[len(OPEN_MARK):], "done": False})
                elif line.strip():
                    # A line typed into the file by hand counts as not done
                    tasks.append({"text": line.strip(), "done": False})
    except FileNotFoundError:
        pass  # First run: nothing has been saved yet
    return tasks

3. Saving after every change

Opening the file with "w" replaces what was there, so the file always matches the list in memory. main() calls this after each add, tick and delete, so closing the program loses nothing.

def save_tasks(file_name, tasks):
    """Write every task to the file, one per line."""
    with open(file_name, "w", encoding="utf-8") as file:
        for task in tasks:
            mark = DONE_MARK if task["done"] else OPEN_MARK
            file.write(mark + task["text"] + "\n")

4. Choosing a task by number

People count from 1 and lists count from 0, so the function returns int(text) - 1. isdecimal() checks the answer is a number before int() runs, and Enter cancels.

def choose_task(tasks, action):
    """Ask for a task number and return its index, or None to cancel."""
    if not tasks:
        print("  There are no tasks yet.")
        return None
    show_tasks(tasks)
    while True:
        text = input(f"Task number to {action} (Enter to cancel): ").strip()
        if text == "":
            return None
        if text.isdecimal() and 1 <= int(text) <= len(tasks):
            return int(text) - 1
        print(f"  Please type a number from 1 to {len(tasks)}.")

5. The menu

Each choice calls a small piece of code and saves the list when something changed. Marking a finished task again gets a message instead of a second save.

def main():
    """Load the list, run the menu, and save after every change."""
    tasks = load_tasks(FILE_NAME)
    print(f"To-do list: {len(tasks)} task(s) loaded from {FILE_NAME}")
    while True:
        print()
        print(MENU)
        choice = input("Choose 1-5: ").strip()
        if choice == "1":
            text = input("New task: ").strip()
            if text:
                tasks.append({"text": text, "done": False})
                save_tasks(FILE_NAME, tasks)
                print(f"  Added: {text}")
            else:
                print("  A task needs some words. Nothing added.")
        elif choice == "2":
            show_tasks(tasks)
        elif choice == "3":
            index = choose_task(tasks, "mark done")
            if index is not None and tasks[index]["done"]:
                print("  That task is already done.")
            elif index is not None:
                tasks[index]["done"] = True
                save_tasks(FILE_NAME, tasks)
                print(f"  Done: {tasks[index]['text']}")
        elif choice == "4":
            index = choose_task(tasks, "delete")
            if index is not None:
                removed = tasks.pop(index)
                save_tasks(FILE_NAME, tasks)
                print(f"  Deleted: {removed['text']}")
        elif choice == "5":
            print(f"Your tasks are saved in {FILE_NAME}. Goodbye.")
            break
        else:
            print("  Please choose a number from 1 to 5.")

Make it bigger

  • Add a due date to each task and list overdue tasks first.
  • Let the user change a task's text without deleting it.
  • Add a Clear done option that removes every finished task at once.
  • Store the tasks as JSON with the json module so each task can carry more fields.

What it practises