Lecture 06: Making Your Programs Remember - Working with Files and Structuring Data
Hello, and welcome to Lecture 06! In our previous lectures, we’ve built command-line applications like the To-Do List. A common question might be: “What happens to your tasks when you close the To-Do List program?” Currently, they disappear! This is because the data was stored in the computer’s temporary memory (RAM). In this lecture, we learn how to make our programs remember information by saving data to files and loading it back. This is called data persistence.
Recap: Organizing Data in Memory
Previously, we’ve used:
- Variables: To store single pieces of information (e.g.,
name = "Priya"
). - Lists: To store ordered collections (e.g.,
tasks = ["Buy milk", "Study Python"]
). - Dictionaries: To store key-value pairs (e.g.,
student = {"name": "Ananya", "id": "S101"}
).
These structures store data in RAM, which is temporary. Files provide permanent storage.
What is File I/O (Input/Output)?
File I/O refers to how programs interact with files on a computer’s disk.
- Files: Named locations on disk (hard drive, SSD) for persistent data storage.
- Input (I): Reading data from a file into your program.
- Output (O): Writing data from your program to a file.
File I/O allows programs to save state, load previous data, and more.
Working with Text Files in Python
Python provides robust ways to handle files.
Introducing pathlib
for Modern Path Management
Before diving into opening files, let’s introduce a modern Python library for handling file paths: pathlib
. While you’ll often see file operations done with simple string paths (e.g., "my_folder/my_file.txt"
), pathlib
offers a more object-oriented and often more readable way to work with paths, especially for complex manipulations or ensuring cross-platform compatibility.
First, you import the Path
object:
from pathlib import Path
Then, you can create Path
objects:
# Create a path to a file in the current directory
my_file_path = Path("my_notes.txt")
# Create a path to a folder
data_folder = Path("data_files")
# Join paths using the / operator (this is a key feature!)
tasks_file_path = data_folder / "tasks.json"
# On Windows, this might become data_files\tasks.json
# On Linux/macOS, it becomes data_files/tasks.json
# pathlib handles these differences for you!
print(f"Path to file: {my_file_path}")
print(f"Path to data folder: {data_folder}")
print(f"Path to tasks file: {tasks_file_path}")
Benefits of pathlib
:
- Readability: Path manipulations are clearer.
- Object-Oriented: Paths are objects with useful methods and properties.
- OS Agnostic: The
/
operator for joining paths works correctly across Windows, macOS, and Linux. - Useful Methods: Easily check if a path exists (
.exists()
), if it’s a file (.is_file()
), get its parent directory (.parent
), etc.
We’ll be using pathlib.Path
objects in many of our file operation examples.
The open()
Function and Path.open()
Method
To work with a file’s content, you first need to open it.
The traditional way is Python’s built-in open()
function:
file_object = open("filename_as_string.txt", "mode")
When using pathlib
, Path
objects have their own .open()
method:
file_object = path_object.open(mode="mode")
Both achieve the same goal: they return a file object (also called a file handle) that your program uses to interact with the actual file on disk.
The arguments are:
filename_as_string
orpath_object
: Specifies the file.mode
: A string indicating how you want to use the file.
File Modes
The mode is crucial:
'r'
(Read Mode - Default): Opens for reading. RaisesFileNotFoundError
if the file doesn’t exist.'w'
(Write Mode): Opens for writing. Overwrites the file if it exists! Creates a new file if it doesn’t.'a'
(Append Mode): Opens for appending. New data is added to the end. Creates the file if it doesn’t exist.'+'
(e.g.,'r+'
): Allows reading and writing.'r+'
requires the file to exist.'b'
(e.g.,'rb'
,'wb'
): Binary mode for non-text files (images, audio).
The with ... .open(...) as file_variable:
Statement (Best Practice!)
It’s vital to close files after use to save changes and free resources. Manually calling file_object.close()
can be error-prone if exceptions occur. The with
statement handles this automatically:
from pathlib import Path
notes_path = Path("my_diary.txt")
# Using pathlib's .open() method with the 'with' statement
with notes_path.open(mode="w") as diary_file:
diary_file.write("Today I learned about pathlib!\n")
diary_file.write("It makes path handling much nicer.\n")
# diary_file is automatically closed here, even if errors happened inside the 'with' block.
# You'll also see Python code that uses open() directly with string paths:
# with open("traditional_notes.txt", "w") as traditional_file:
# traditional_file.write("This also works.\n")
# This is the traditional way and still works perfectly fine. However, pathlib
# is often preferred in modern Python for its clarity and power, especially when
# you need to do more than just open a file (like checking if it exists,
# getting its parent directory, etc.).
Always use the with ... .open(...) as ...:
syntax for file operations.
Reading from Files
Let’s assume greet.txt
contains:
Hello Priya!
Welcome to Python.
-
Path.read_text(encoding='utf-8')
: Reads Entire Text File (withpathlib
)- A convenient
pathlib
method to read the entire file into a single string. encoding='utf-8'
is good practice for text files to handle a wide range of characters.
from pathlib import Path greet_path = Path("greet.txt") # Assume greet.txt exists with "Hello Priya!\nWelcome to Python." # To make this runnable, let's create greet.txt first for the example # greet_path.write_text("Hello Priya!\nWelcome to Python.\n", encoding="utf-8") try: if greet_path.exists(): # Check if file exists using pathlib full_content = greet_path.read_text(encoding="utf-8") print("--- Reading with path.read_text() ---") print(full_content) else: print(f"Error: {greet_path} was not found!") except Exception as e: print(f"An error occurred: {e}")
- A convenient
-
Looping Directly Over the File Object (Line-by-Line):
- Most memory-efficient for large files.
from pathlib import Path greet_path = Path("greet.txt") # Assume greet.txt exists # To make this runnable, let's ensure greet.txt exists for the example # greet_path.write_text("Hello Priya!\nWelcome to Python.\n", encoding="utf-8") try: with greet_path.open(mode="r", encoding="utf-8") as my_file: print("\n--- Looping directly (line by line) ---") for current_line in my_file: print(f"Line: {current_line.strip()}") # .strip() removes \n except FileNotFoundError: print(f"Error: {greet_path} not found!")
.strip()
: Removes leading/trailing whitespace, especially the newline (\n
) character from lines read. -
Other
file_object
methods (can be used withpath_object.open()
):my_file.read()
: Reads entire file (if opened withpath_object.open()
).my_file.readline()
: Reads one line.my_file.readlines()
: Reads all lines into a list.
Writing to Files
-
Path.write_text(text_content, encoding='utf-8')
: Writes Entire String (withpathlib
)- Convenient for writing a whole string (which might contain newlines) to a file.
- Overwrites the file if it exists, or creates it.
from pathlib import Path output_path = Path("my_story.txt") story_content = "Once upon a time, in a land of code...\nPython scripts danced freely.\n" try: output_path.write_text(story_content, encoding="utf-8") print(f"Story written to {output_path}") except Exception as e: print(f"Error writing story: {e}")
-
file_object.write(string_data)
(used withpath_object.open()
):- Requires manual
\n
for new lines. Numbers must bestr()
.
from pathlib import Path notes_path = Path("more_notes.txt") with notes_path.open(mode="w", encoding="utf-8") as notes_file: notes_file.write("Rohan's note.\n") notes_file.write(f"Items to buy: {str(5)}\n") print(f"More notes written to {notes_path}")
- Requires manual
Structuring Data in Files (To-Do List Example)
Our To-Do list (tasks = ['Buy groceries', 'Call Priya']
) needs to be saved and loaded.
Method 1: Simple Text File - One Task Per Line
Saving:
from pathlib import Path
# tasks_in_memory = ["Buy groceries", "Call Priya"]
# file_path = Path("mytasks_simple.txt")
#
# # Join all tasks with a newline character in between
# text_to_save = "\n".join(tasks_in_memory)
# if tasks_in_memory: # Add a final newline if there are tasks to ensure last line ends with \n
# text_to_save += "\n"
#
# try:
# file_path.write_text(text_to_save, encoding="utf-8")
# print(f"Tasks saved to {file_path}")
# except Exception as e:
# print(f"Error saving tasks: {e}")
Loading:
from pathlib import Path
# loaded_tasks_list = []
# file_path = Path("mytasks_simple.txt")
#
# try:
# if file_path.exists():
# text_content = file_path.read_text(encoding="utf-8")
# # .strip() removes any leading/trailing blank lines from the whole content
# # .splitlines() splits the string into a list at newline characters
# # and handles the last line not having a newline correctly.
# loaded_tasks_list = text_content.strip().splitlines()
# else:
# print(f"Task file '{file_path}' not found. Starting empty.")
# except Exception as e:
# print(f"Error loading tasks: {e}")
#
# print(f"Loaded tasks: {loaded_tasks_list}")
- Pros: Simple, human-readable.
- Cons: Difficult for complex data (e.g., tasks with due dates, status).
\n
in task descriptions would break it.
Method 2: Using JSON for Structured and Robust Storage
JSON (JavaScript Object Notation) is a text format for storing structured data, mapping well to Python lists and dictionaries.
Python’s json
Module:
import json
- Saving with
json.dump(python_object, file_object, indent=optional)
:
Writes a Python list/dict to a file in JSON.indent=4
for readability.# import json # from pathlib import Path # tasks_to_save = ["Buy milk", "Learn JSON"] # json_file_path = Path("mytasks.json") # # with json_file_path.open(mode="w", encoding="utf-8") as f_out: # json.dump(tasks_to_save, f_out, indent=4)
- Loading with
python_object = json.load(file_object)
:
Reads JSON from a file into a Python list/dict.# import json # from pathlib import Path # json_file_path = Path("mytasks.json") # loaded_tasks = [] # if json_file_path.exists(): # with json_file_path.open(mode="r", encoding="utf-8") as f_in: # loaded_tasks = json.load(f_in)
Advantages: Handles complex data and special characters well. More robust.
Project: Enhancing the To-Do List App with File Persistence (using JSON and pathlib
)
We will now enhance our To-Do list application to save tasks to a .json
file using pathlib
.
Planning the Persistence Feature
- Filename:
todolist_data.json
(managed by aPath
object). - Functions:
load_tasks_from_file(filepath: Path)
save_tasks_to_file(tasks_list: list, filepath: Path)
- Integration: Load on start, save on any change (add/remove).
Building the load_tasks_from_file
Function
import json
from pathlib import Path
def load_tasks_from_file(filepath: Path):
"""Loads tasks from a JSON file using pathlib.Path.
Returns an empty list if the file doesn't exist or is invalid.
"""
try:
if filepath.exists() and filepath.is_file():
with filepath.open(mode='r', encoding='utf-8') as f:
tasks = json.load(f)
if not isinstance(tasks, list): # Validate structure
print(f"Warning: Data in {filepath} is not a list. Starting fresh.")
return []
return tasks
else:
# No error message needed if file simply doesn't exist (first run)
return []
except json.JSONDecodeError:
print(f"Error: Could not decode JSON from '{filepath}'. File might be corrupted.")
return []
except Exception as e:
print(f"An unexpected error occurred loading tasks: {e}")
return []
Building the save_tasks_to_file
Function
# import json # Already imported
# from pathlib import Path # Already imported
def save_tasks_to_file(tasks_list: list, filepath: Path):
"""Saves the current list of tasks to a JSON file using pathlib.Path."""
try:
# Ensure parent directory exists (optional, good for complex paths)
# filepath.parent.mkdir(parents=True, exist_ok=True)
with filepath.open(mode='w', encoding='utf-8') as f:
json.dump(tasks_list, f, indent=4)
except Exception as e:
print(f"An error occurred while saving tasks to '{filepath}': {e}")
Modifying the Main Application Logic
The main run_todo_app
function from Lecture 04 needs to be adapted.
(The following shows conceptual changes integrated into a full application structure below).
Complete Code: todo_list_app_persistent.py
Here is the complete code for the enhanced To-Do List application, now using pathlib
and JSON for persistence.
# examples/todo_list/todo_list_app_persistent.py
import json
from pathlib import Path
TASKS_FILE_PATH = Path("todolist_data.json") # Using pathlib
# --- File Operations ---
def load_tasks_from_file(filepath: Path):
"""Loads tasks from a JSON file using pathlib.Path."""
try:
if filepath.exists() and filepath.is_file():
with filepath.open(mode='r', encoding='utf-8') as f:
tasks_data = json.load(f)
if not isinstance(tasks_data, list):
print(f"Warning: Data in {filepath} is not a list. Starting fresh.")
return []
print(f"Tasks loaded from {filepath}")
return tasks_data
else:
return [] # No file found, start with empty list
except json.JSONDecodeError:
print(f"Error: Corrupted data in {filepath}. Starting fresh.")
return []
except Exception as e:
print(f"An unexpected error occurred loading tasks: {e}")
return []
def save_tasks_to_file(tasks_list: list, filepath: Path):
"""Saves the current list of tasks to a JSON file using pathlib.Path."""
try:
with filepath.open(mode='w', encoding='utf-8') as f:
json.dump(tasks_list, f, indent=4)
# print(f"Tasks saved to {filepath}") # Optional confirmation
except Exception as e:
print(f"An error occurred while saving tasks: {e}")
# --- Core To-Do Functions ---
def display_menu():
print("\n===== Persistent To-Do List Menu =====")
print("1. Add a new task")
print("2. View all tasks")
print("3. Remove a task (by number)")
print("4. Quit application")
print("====================================")
def add_task_action(tasks_list: list):
task_description = input("Enter the description of the new task: ")
cleaned_description = task_description.strip()
if cleaned_description:
tasks_list.append(cleaned_description)
print(f"Task '{cleaned_description}' added.")
return True # Indicate success for saving
else:
print("Task description cannot be empty.")
return False
def view_tasks_action(tasks_list: list):
print("\n--- Your Tasks ---")
if not tasks_list:
print("Your to-do list is currently empty.")
else:
for index, task in enumerate(tasks_list, start=1):
print(f"{index}. {task}")
print("------------------")
def remove_task_action(tasks_list: list):
view_tasks_action(tasks_list)
if not tasks_list:
return False
try:
task_number_str = input("Enter the number of the task to remove: ")
task_number_to_remove = int(task_number_str)
if 1 <= task_number_to_remove <= len(tasks_list):
task_index_to_remove = task_number_to_remove - 1
removed_task = tasks_list.pop(task_index_to_remove)
print(f"Task '{removed_task}' removed.")
return True # Indicate success for saving
else:
print(f"Invalid task number. Please enter a number between 1 and {len(tasks_list)}.")
return False
except ValueError:
print("Invalid input. Please enter a number.")
return False
except Exception as e:
print(f"An unexpected error occurred during removal: {e}")
return False
# --- Main Application Logic ---
def run_persistent_todo_app():
current_tasks = load_tasks_from_file(TASKS_FILE_PATH)
while True:
display_menu()
choice = input("Enter your choice (1-4): ")
if choice == '1':
if add_task_action(current_tasks):
save_tasks_to_file(current_tasks, TASKS_FILE_PATH)
elif choice == '2':
view_tasks_action(current_tasks)
elif choice == '3':
if remove_task_action(current_tasks):
save_tasks_to_file(current_tasks, TASKS_FILE_PATH)
elif choice == '4':
print("Exiting To-Do List. Your tasks are saved. Goodbye!")
break
else:
print("Invalid choice. Please enter a number between 1 and 4.")
input("\nPress Enter to continue...")
if __name__ == "__main__":
run_persistent_todo_app()
Understanding the Persistent To-Do List App: A Code Walkthrough
Let’s break down how the todo_list_app_persistent.py
script works, focusing on the new persistence features.
-
Overall Structure:
- Imports: We start by importing
json
to work with JSON data andPath
from thepathlib
module for easier file path management. TASKS_FILE_PATH = Path("todolist_data.json")
: A constant is defined usingPath
to represent the file where tasks will be stored. UsingPath
makes the code cleaner and more adaptable to different operating systems.- The
tasks
list, which used to be initialized as empty at the start of the main function, will now be populated byload_tasks_from_file
. (In the provided full code,current_tasks
inrun_persistent_todo_app
serves this role).
- Imports: We start by importing
-
Function
display_menu()
:- This is a simple helper function. Its only job is to print the menu options to the user, making the main loop cleaner. It doesn’t take any arguments or return anything.
-
Function
load_tasks_from_file(filepath: Path)
:- Purpose: This crucial function is responsible for loading the tasks from our JSON file when the application starts.
filepath: Path
Argument: It expects aPath
object representing the location of the data file.- File Existence Check:
if filepath.exists() and filepath.is_file():
It first checks if the path exists and if it’s actually a file (not a directory) before trying to open it. This is a good practice withpathlib
. - Opening and Reading:
with filepath.open(mode='r', encoding='utf-8') as f:
opens the file in read mode ('r'
).encoding='utf-8'
is specified for broad character compatibility. json.load(f)
: This is where the magic happens.json.load()
reads the text content from the file objectf
, parses the JSON structure, and converts it into a Python data structure. Since we saved our tasks as a JSON array, this will return a Python list.- Validation:
if not isinstance(tasks_data, list):
is a safety check. If the JSON file was somehow corrupted or manually changed to not contain a list at its top level, this prevents errors later by returning an empty list. FileNotFoundError
Handling: Thetry...except FileNotFoundError
block (implicitly handled byfilepath.exists()
in this version, but good to rememberopen()
can raise it) ensures that if thetodolist_data.json
file doesn’t exist (e.g., when the user runs the application for the very first time), the program doesn’t crash. Instead, it returns an empty list[]
, allowing the application to start with a fresh to-do list.json.JSONDecodeError
Handling: If the file exists but contains text that isn’t valid JSON (e.g., it’s corrupted or was manually edited incorrectly),json.load(f)
would raise ajson.JSONDecodeError
. Theexcept
block catches this, prints a warning, and returns an empty list, preventing a crash.- Generic
Exception
: Catches any other unexpected I/O errors during loading.
-
Function
save_tasks_to_file(tasks_list: list, filepath: Path)
:- Purpose: This function takes the current list of tasks (
tasks_list
) from the application’s memory and writes it to the JSON file specified byfilepath
. filepath.open(mode='w', encoding='utf-8')
: The file is opened in write mode ('w'
). This means if the file already exists, its old content will be completely overwritten with the new data. This is exactly what we want to save the current state of the to-do list. If the file doesn’t exist, it will be created.json.dump(tasks_list, f, indent=4)
: This function does the work of converting the Pythontasks_list
into JSON format and writing it to the file objectf
.indent=4
: This argument is very helpful for us humans! It tellsjson.dump
to “pretty-print” the JSON data with an indentation of 4 spaces for each level of nesting. This makes thetodolist_data.json
file easy to open in a text editor and inspect. Without it, the entire JSON array would often be written on a single, long line.
- Generic
Exception
: Catches potential errors during saving (e.g., disk full, file permissions).
- Purpose: This function takes the current list of tasks (
-
Function
add_task_action(tasks_list: list)
: (Assuming it modifies the passed list)- It prompts the user for a new task description.
- It uses
.strip()
to remove any accidental leading or trailing spaces from the user’s input. - It checks if the
cleaned_description
is not empty before appending. tasks_list.append(cleaned_description)
: The new task is added to the list that was passed into the function.- It returns
True
if a task was added,False
otherwise, so the main loop knows whether to save.
-
Function
view_tasks_action(tasks_list: list)
:- Its purpose is to display all the tasks currently stored in the
tasks_list
. - It first checks if the
tasks_list
is empty and prints an appropriate message if it is. - If there are tasks, it uses
enumerate(tasks_list, start=1)
to loop through the tasks.enumerate
provides both the index (we start it at 1 for user-friendliness) and the task itself. - Each task is then printed with its number.
- Its purpose is to display all the tasks currently stored in the
-
Function
remove_task_action(tasks_list: list)
:- First, it calls
view_tasks_action(tasks_list)
so the user can see the numbers of the tasks they might want to remove. - It checks if the list is empty; if so, it returns
False
. - It prompts the user to enter the number of the task they wish to remove.
- Input Validation:
task_number_to_remove = int(task_number_str)
: It tries to convert the user’s input string into an integer. If this fails (e.g., the user types “abc”), aValueError
is caught, an error message is printed, and it returnsFalse
.if 1 <= task_number_to_remove <= len(tasks_list)
: It checks if the entered number is within the valid range of the displayed task numbers.
- Index Conversion:
task_index_to_remove = task_number_to_remove - 1
converts the 1-based number from the user into a 0-based index suitable for Python lists. removed_task = tasks_list.pop(task_index_to_remove)
: The task at the calculated index is removed from the list..pop()
also returns the removed item, which is used in the confirmation message.- It returns
True
if a task was successfully removed.
- First, it calls
-
Function
run_persistent_todo_app()
(Main Application Logic):- Initial Load:
current_tasks = load_tasks_from_file(TASKS_FILE_PATH)
is the very first significant action. This attempts to load any tasks saved from a previous session into thecurrent_tasks
list. If no file is found or it’s invalid,current_tasks
will be an empty list. - Main Loop (
while True
): This loop keeps the application running and presenting the menu until the user chooses to quit. display_menu()
is called in each iteration to show the user their options.- The user’s
choice
is read. if/elif/else
for Choices:- If ‘1’ (Add):
add_task_action(current_tasks)
is called. If it returnsTrue
(meaning a task was actually added),save_tasks_to_file(current_tasks, TASKS_FILE_PATH)
is immediately called to make the change persistent. - If ‘2’ (View):
view_tasks_action(current_tasks)
is called. - If ‘3’ (Remove):
remove_task_action(current_tasks)
is called. If it returnsTrue
(a task was removed),save_tasks_to_file(current_tasks, TASKS_FILE_PATH)
is called to persist the removal. - If ‘4’ (Quit): A goodbye message is printed, and
break
is executed to terminate thewhile
loop, thus ending the program. - Else (Invalid choice): An error message is shown.
- If ‘1’ (Add):
input("\nPress Enter to continue...")
: This simple line pauses the program after each action, waiting for the user to press Enter. This prevents the menu from instantly reappearing and gives the user time to read messages (like “Task added successfully!”).
- Initial Load:
-
The
if __name__ == "__main__":
block:- This is a standard Python idiom. It ensures that the
run_persistent_todo_app()
function (which starts the whole application) is called only when the script (todo_list_app_persistent.py
) is executed directly by the Python interpreter (e.g.,python todo_list_app_persistent.py
). - If this script were to be imported as a module into another Python file, the code inside this
if
block would not automatically run, which is generally the desired behavior for reusable modules.
- This is a standard Python idiom. It ensures that the
This walkthrough should clarify how each part of the persistent To-Do List app functions, with a special focus on how file operations are integrated to load and save tasks.
How to Run & Test
- Save: Save the code as
todo_list_app_persistent.py
in a directory likeexamples/todo_list/
. - Run: Open your terminal, navigate to that directory (
cd examples/todo_list
), and runpython todo_list_app_persistent.py
. - Test: Add tasks, view them, remove some, then quit. Run the app again. Your tasks should be loaded from
todolist_data.json
(which will be created in the same directory). Inspect this file to see the JSON structure.
Concepts Learned Recap
This lecture covered essential skills for making your programs more robust and useful:
- File I/O: Reading from and writing to files for data persistence.
pathlib.Path
: UsingPath
objects for modern, object-oriented path management (e.g.,Path("file.txt")
,data_dir / "file.json"
,my_path.exists()
,my_path.read_text()
,my_path.write_text()
).- File Modes: Understanding
'r'
,'w'
,'a'
. with my_path.open(...) as f:
: The standard way to ensure files are properly closed.- Reading Methods: Using
f.read()
,f.readline()
,f.readlines()
, and looping directly overf
.Path.read_text()
. - Writing Methods: Using
f.write()
(and adding\n
),f.writelines()
.Path.write_text()
. .strip()
: Removing whitespace (especially newlines) from read lines.- JSON for Structured Data: Using the
json
module (import json
) withjson.dump()
(to save Python lists/dicts to a JSON file) andjson.load()
(to load JSON from a file into Python lists/dicts). Usingindent
for readable JSON. - Error Handling:
try-except FileNotFoundError
andtry-except json.JSONDecodeError
for robust file operations.
Homework Challenges
-
Simple Personal Journal App:
- Create an app to write and view journal entries.
- Entries are strings, saved to
journal.txt
(perhaps usepathlib
andPath.write_text
in append mode, or read all, append, then rewrite). - Consider adding a timestamp to each entry (
import datetime; datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
).
-
High Score System (JSON):
- Modify “Guess the Number” to save high scores (name and attempts) to
high_scores.json
. - Store as a list of dictionaries:
[{"name": "Rohan", "attempts": 5}]
. - Load, update (sort by attempts, keep top N), and save scores. Display them.
- Modify “Guess the Number” to save high scores (name and attempts) to
-
CSV Data for To-Do List:
- Modify the persistent To-Do list to use CSV format instead of JSON. Each line in
tasks.csv
could betask_description,status
(e.g., “Buy milk,pending”). - Use string
.split(',')
for reading and f-strings or','.join()
for writing. - (Consider how to handle commas within task descriptions – this shows a limitation of simple CSV vs. JSON).
- Modify the persistent To-Do list to use CSV format instead of JSON. Each line in