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

Math Behind Logic Gates

6502 - Part 2 Reset and Clock Circuit

Building a 6502 NOP Test