Learn Python in 30 Days — Day 21 Week 3 Challenge: Daily Journal CLI App

    Day 21 - Challenge: Daily Journal CLI App 

Welcome to Day 21 of the Learn Python in 30 Days series!

By now you’ve used:

  • Functions (Day 15–16)

  • Error handling with try / except (Day 17)

  • File handling (Day 18)

  • Modules (Day 19–20)

Today, we’re going to glue all of that together and build a proper little project:

A Daily Journal CLI App that’s split into:

  • journal_core.py → your self-made module with all the logic

  • journal_cli.py → a tiny command-line interface that imports and uses it 

The goal isn’t just “make a journal”, it’s to think like a developer: split code into sensible pieces, give each function a job, and wire it all together.

All example files for this series are available on my GitHub: Learn-Python-in-30-Days

What You’ll Build

A core module: journal_core.py

Which handles:

  • getting today’s date

  • adding entries

  • viewing all entries

  • viewing entries for a specific date

  • file + error handling

A CLI script: journal_cli.py

Which handles:

  • showing the menu

  • reading user choices

  • calling functions from journal_core

Let’s build this step by step.

Plan the Journal Format

Before any code, decide how you’ll store entries.

We’ll keep it simple: one text file called journal.txt.

Each entry looks like this:

=== 2025-11-21 === Today I started my Python journal app. It’s actually pretty cool. === 2025-11-22 === Learned about error handling. try/except isn’t scary anymore.

Rules:

  • Each entry starts with a header line:
    === YYYY-MM-DD ===

  • The lines after that are the entry text

  • A blank line separates entries

Everything we build in journal_core.py will read and write this format.

Create journal_core.py

Create a new file called:

journal_core.py

Add this at the top:

""" journal_core.py Core logic for the Daily Journal app: - getting today's date - adding entries - viewing entries """ from datetime import date JOURNAL_FILE = "journal.txt"

What’s going on?

  • The docstring at the top explains what this file is for.
    This is really helpful when your project grows.

  • from datetime import date imports the date class so we can get today’s date later.

  • JOURNAL_FILE = "journal.txt" is a module-level constant.
    Every function that needs the journal file uses this same name, so if you ever change it, you do it in one place.

Step 1 – get_today_str() - Getting todays date

Now we need a standard way to get today’s date as a string.

Add this to journal_core.py:

def get_today_str(): """Return today's date as a YYYY-MM-DD string.""" return date.today().isoformat()

What’s going on?

  • date.today() gives you today’s date as a date object.

  • .isoformat() turns it into "YYYY-MM-DD", which is perfect for your header lines.

  • Wrapping this in a function means:

    • You don’t repeat this logic everywhere.

    • If you ever want a different format, you change it in one place.

We’ll use get_today_str() in the next function.

Step 2 – add_entry_for_today() — Writing New Entries

Now to add functions for writing a new entry.

def add_entry_for_today():

"""Let the user type a journal entry for today and save it.""" today = get_today_str() print(f"\nWriting entry for {today}.") print("Type your entry. Press Enter on an empty line to finish.\n") lines = [] while True: line = input() if line == "": break lines.append(line) if not lines: print("No text entered. Entry cancelled.") return entry_text = "\n".join(lines) try: with open(JOURNAL_FILE, "a", encoding="utf-8") as f: f.write(f"=== {today} ===\n") f.write(entry_text + "\n\n") print("Entry saved!") except OSError as e: print("Something went wrong while saving your entry:") print(e)

What’s going on?

1. Getting today’s date

today = get_today_str()
  • Keeps the date format consistent everywhere.

2. Collecting multi-line input

lines = [] while True: line = input() if line == "": break lines.append(line)
  • The user types lines of text.

  • When they press Enter on an empty line, the loop stops.

  • All non-empty lines get stored in lines.

3. Handling “no entry”

if not lines: print("No text entered. Entry cancelled.") return
  • If the user just presses Enter straight away, we don’t write an empty entry to the file.

4. Joining lines into a single block of text

entry_text = "\n".join(lines)
  • Turns ["Line 1", "Line 2"] into:

    Line 1 Line 2

5. Writing to the file with error handling

try: with open(JOURNAL_FILE, "a", encoding="utf-8") as f: f.write(f"=== {today} ===\n") f.write(entry_text + "\n\n") print("Entry saved!") except OSError as e: print("Something went wrong while saving your entry:") print(e)
  • "a" = append mode, so we don’t overwrite old entries.

  • We write:

    • The header line with the date

    • The entry text

    • A blank line afterwards

  • If anything goes wrong (e.g. permission issue, disk problem), OSError is caught and we print a friendly error.

Step 3 – view_all_entries() — Reading the Entire Journal

Next, we want to see everything we’ve ever written.

def view_all_entries():

"""Read and print all journal entries.""" print("\n=== All Journal Entries ===\n") try: with open(JOURNAL_FILE, "r", encoding="utf-8") as f: content = f.read() except FileNotFoundError: print("No journal file found yet. Write your first entry!") return except OSError as e: print("Could not read the journal file:") print(e) return if not content.strip(): print("Journal is empty for now.") else: print(content)

What’s going on?

1. Simple heading

print("\n=== All Journal Entries ===\n")
  • Just gives a clear title before we show content.

2. Reading the file safely

try: with open(JOURNAL_FILE, "r", encoding="utf-8") as f: content = f.read()
  • "r" = read mode.

  • f.read() loads the entire file into a single string.

3. Handling file-related errors

except FileNotFoundError: print("No journal file found yet. Write your first entry!") return except OSError as e: print("Could not read the journal file:") print(e) return
  • FileNotFoundError → This is normal if you haven’t written any entries yet.

  • OSError → Catches other I/O issues and shows a message.

4. Handling an empty file

if not content.strip(): print("Journal is empty for now.") else: print(content)
  • content.strip() removes whitespace.
    If the result is empty, the file contains nothing meaningful yet.

Step 4 – view_entries_for_date() — Filtering by Date

Now a slightly more advanced function: show entries for a specific date.

def view_entries_for_date(target_date):

"""Show entries that match a specific date (YYYY-MM-DD).""" header = f"=== {target_date} ===" try: with open(JOURNAL_FILE, "r", encoding="utf-8") as f: lines = f.readlines() except FileNotFoundError: print("No journal file found yet. Write your first entry!") return except OSError as e: print("Could not read the journal file:") print(e) return matching_entries = [] current_entry_lines = [] in_matching_entry = False for line in lines: if line.startswith("===") and "===" in line: # starting a new entry block if in_matching_entry and current_entry_lines: matching_entries.append("".join(current_entry_lines)) current_entry_lines = [] if line.strip() == header: in_matching_entry = True current_entry_lines.append(line) else: in_matching_entry = False else: if in_matching_entry: current_entry_lines.append(line) # catch last entry if in_matching_entry and current_entry_lines: matching_entries.append("".join(current_entry_lines)) if not matching_entries: print(f"No entries found for {target_date}.") else: print(f"\n=== Entries for {target_date} ===\n") for entry in matching_entries: print(entry) print("-" * 40)

What’s going on?

1. Build the header we’re looking for

header = f"=== {target_date} ==="
  • If target_date is "2025-11-21", the header becomes:

    === 2025-11-21 ===

2. Read the file as lines

with open(JOURNAL_FILE, "r", encoding="utf-8") as f: lines = f.readlines()
  • We read line by line because we need to figure out which lines belong to which entry.

3. Variables for tracking state

matching_entries = [] current_entry_lines = [] in_matching_entry = False
  • matching_entries — list of full entries for the specified date.

  • current_entry_lines — lines for the entry we’re currently reading.

  • in_matching_entryTrue when we’re inside an entry with the target date.

4. Loop through each line to detect entry blocks

for line in lines: if line.startswith("===") and "===" in line: # starting a new entry block if in_matching_entry and current_entry_lines: matching_entries.append("".join(current_entry_lines)) current_entry_lines = [] if line.strip() == header: in_matching_entry = True current_entry_lines.append(line) else: in_matching_entry = False else: if in_matching_entry: current_entry_lines.append(line)
  • If a line looks like a header (=== something ===):

    • We’re starting a new entry.

    • If we were just inside a matching entry, we save the lines we collected so far.

    • Then we check if the new header matches our target date.

  • If it’s not a header and in_matching_entry is True:

    • The line belongs to the current entry and is added to current_entry_lines.

5. Catch the last entry

if in_matching_entry and current_entry_lines: matching_entries.append("".join(current_entry_lines))
  • After the loop, the last entry won’t have been saved yet, so we add it now if needed.

6. Print results

if not matching_entries: print(f"No entries found for {target_date}.") else: print(f"\n=== Entries for {target_date} ===\n") for entry in matching_entries: print(entry) print("-" * 40)
  • If nothing matched: friendly message.

  • If there are matches: print each entry with a separator line.

Build journal_cli.py

Now that journal_core.py has all the logic, we make a tiny script to run it.

Create a new file:

journal_cli.py

Add this code:

""" journal_cli.py Command-line interface for the Daily Journal app. Uses functions from journal_core.py. """ from journal_core import ( add_entry_for_today, view_all_entries, view_entries_for_date, ) def show_menu(): print("\n=== Daily Journal ===") print("1) Add entry for today") print("2) View all entries") print("3) View entries for a specific date") print("4) Quit") def main(): while True: show_menu() choice = input("Choose an option (1-4): ").strip() if choice == "1": add_entry_for_today() elif choice == "2": view_all_entries() elif choice == "3": target_date = input("Enter date (YYYY-MM-DD): ").strip() if target_date: view_entries_for_date(target_date) else: print("No date entered.") elif choice == "4": print("Goodbye!") break else: print("Invalid choice, please try again.") if __name__ == "__main__": main()

What’s going on?

1. Importing your own module

from journal_core import ( add_entry_for_today, view_all_entries, view_entries_for_date, )
  • journal_core is your self-made module.

  • You import the functions you need, just like you do with math or random.

2. show_menu() – UI only, no logic

def show_menu(): print("\n=== Daily Journal ===") print("1) Add entry for today") print("2) View all entries") print("3) View entries for a specific date") print("4) Quit")
  • This function just prints the menu.

  • It doesn’t talk to files or dates — that’s journal_core’s job.

3. main() – the main loop

def main(): while True: show_menu() choice = input("Choose an option (1-4): ").strip() if choice == "1": add_entry_for_today() elif choice == "2": view_all_entries() elif choice == "3": target_date = input("Enter date (YYYY-MM-DD): ").strip() if target_date: view_entries_for_date(target_date) else: print("No date entered.") elif choice == "4": print("Goodbye!") break else: print("Invalid choice, please try again.")
  • Shows the menu.

  • Reads the user’s choice.

  • Calls the relevant function from journal_core.

  • Keeps looping until the user chooses "4" to quit.

4. The usual “run this file directly” guard

if __name__ == "__main__": main()
  • This means main() only runs if you do:

    python journal_cli.py
  • If you imported journal_cli from somewhere else, it wouldn’t auto-run.

Run the Journal App

Now that the coding is completed you can run the program and it should look something like that seen below: -
You can also download the two files and run them from the links below: -

Reusing Your Module Elsewhere

The nice part about this setup?

journal_core is now a reusable module. You can use it in other scripts:

from journal_core import add_entry_for_today add_entry_for_today()

You could write a completely different interface but still call the same core functions.

Next Up — Day 22 – Lists & Dictionary Comprehensions

Tomorrow is Day 22 — Week 4, where you’ll begin structuring programs, tie all skills together and complete one final project

All example files for this series are available on my GitHub: Learn-Python-in-30-Days

You can see the full series here Learn Python in 30 Days series!

Hope you have enjoyed this post, thanks Matty

Comments

Popular posts from this blog

6502 - Part 2 Reset and Clock Circuit

Math Behind Logic Gates

BBC Model B + MMFS: Command Guide