Personal Diary Application
Abstract
Section titled “Abstract”A personal diary is one of those projects where everything you build is genuinely useful. In this tutorial you will build a command-line journal that lets you write timestamped entries, tag each one with a mood, search through your past, filter by mood, and see stats on your journaling habit. Entries persist between runs via a JSON file you control entirely.
You will learn:
- How to model entries as a class (and how
@dataclasscuts that boilerplate). - How to read and write JSON with
json.dump/json.load. - How to work with timestamps via
datetime. - How to build a clean, menu-driven CLI without spaghetti.
- How to add real privacy with optional encryption.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above (3.7+ recommended for
dataclasses). - A text editor or IDE.
- Comfort with functions, lists, and dictionaries.
- Familiarity with running a
.pyfile from the terminal.
Concepts You Will Use
Section titled “Concepts You Will Use”| Concept | Purpose |
|---|---|
| Class | Group related data (entry fields) and behavior. |
@dataclass | Auto-generate __init__, __repr__, comparison — less boilerplate. |
json.dump / json.load | Persist Python objects to a text file as JSON. |
datetime | Timestamp entries with date and time. |
Path / os.path.exists | Detect whether the diary file is new or existing. |
| List comprehensions | Filter and search entries cleanly. |
flowchart TD
n0(["script start"])
n2["main()"]
subgraph PersonalDiary
n87["__init__()"]
n88["add_entry()"]
n89["filter_by_mood()"]
n90["get_statistics()"]
n91["load_entries()"]
n92["save_entries()"]
n93["search_entries()"]
n94["view_entries()"]
end
n87 --> n91
n88 --> n92
n0 --> n2
n2 --> n88
n2 --> n89
n2 --> n90
n2 --> n93
n2 --> n94
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create a folder named
personal-diary. - Inside it, create
personaldiary.py. - (Optional) create
diary.json— the script will create it for you on first save.
Write the code
Section titled “Write the code”Personal Diary
pch.viewSource# Personal Diary Application
import datetime
import os
import json
def ask(prompt="", default=""):
"""Read a line, or fall back to `default` when nobody is there to type.
Without this the script raises EOFError the moment it runs unattended — in
a test, a scheduled job, or the build that captures this output for the
docs. The fallback is printed rather than silent, so a reader can always
tell which answers were typed and which were assumed.
"""
try:
return input(prompt).strip() or default
except EOFError:
print(f"{default} (no input available, using the default)")
return default
class DiaryEntry:
def __init__(self, date, title, content, mood="neutral"):
self.date = date
self.title = title
self.content = content
self.mood = mood
def to_dict(self):
return {
'date': self.date.isoformat(),
'title': self.title,
'content': self.content,
'mood': self.mood
}
@classmethod
def from_dict(cls, data):
return cls(
datetime.datetime.fromisoformat(data['date']),
data['title'],
data['content'],
data.get('mood', 'neutral')
)
class PersonalDiary:
def __init__(self, filename="diary.json"):
self.filename = filename
self.entries = []
self.load_entries()
def load_entries(self):
"""Load entries from file"""
if os.path.exists(self.filename):
try:
with open(self.filename, 'r') as f:
data = json.load(f)
self.entries = [DiaryEntry.from_dict(entry) for entry in data]
except (json.JSONDecodeError, KeyError):
print("Warning: Could not load diary entries. Starting fresh.")
self.entries = []
def save_entries(self):
"""Save entries to file"""
with open(self.filename, 'w') as f:
json.dump([entry.to_dict() for entry in self.entries], f, indent=2)
def add_entry(self, title, content, mood="neutral"):
"""Add a new diary entry"""
entry = DiaryEntry(datetime.datetime.now(), title, content, mood)
self.entries.append(entry)
self.save_entries()
print("Entry added successfully!")
def view_entries(self):
"""Display all diary entries"""
if not self.entries:
print("No diary entries found.")
return
print("\n" + "="*50)
print("YOUR DIARY ENTRIES")
print("="*50)
for i, entry in enumerate(self.entries, 1):
print(f"\nEntry #{i}")
print(f"Date: {entry.date.strftime('%Y-%m-%d %H:%M')}")
print(f"Title: {entry.title}")
print(f"Mood: {entry.mood}")
print(f"Content: {entry.content}")
print("-" * 30)
def search_entries(self, keyword):
"""Search entries by keyword"""
found_entries = []
keyword_lower = keyword.lower()
for entry in self.entries:
if (keyword_lower in entry.title.lower() or
keyword_lower in entry.content.lower()):
found_entries.append(entry)
if not found_entries:
print(f"No entries found containing '{keyword}'")
return
print(f"\nFound {len(found_entries)} entries containing '{keyword}':")
print("="*50)
for i, entry in enumerate(found_entries, 1):
print(f"\nResult #{i}")
print(f"Date: {entry.date.strftime('%Y-%m-%d %H:%M')}")
print(f"Title: {entry.title}")
print(f"Mood: {entry.mood}")
print(f"Content: {entry.content}")
print("-" * 30)
def filter_by_mood(self, mood):
"""Filter entries by mood"""
mood_entries = [entry for entry in self.entries if entry.mood.lower() == mood.lower()]
if not mood_entries:
print(f"No entries found with mood '{mood}'")
return
print(f"\nEntries with mood '{mood}':")
print("="*50)
for i, entry in enumerate(mood_entries, 1):
print(f"\nEntry #{i}")
print(f"Date: {entry.date.strftime('%Y-%m-%d %H:%M')}")
print(f"Title: {entry.title}")
print(f"Content: {entry.content}")
print("-" * 30)
def get_statistics(self):
"""Display diary statistics"""
if not self.entries:
print("No entries to analyze.")
return
total_entries = len(self.entries)
mood_counts = {}
for entry in self.entries:
mood = entry.mood
mood_counts[mood] = mood_counts.get(mood, 0) + 1
print(f"\nDiary Statistics:")
print("="*30)
print(f"Total entries: {total_entries}")
print(f"First entry: {min(self.entries, key=lambda x: x.date).date.strftime('%Y-%m-%d')}")
print(f"Latest entry: {max(self.entries, key=lambda x: x.date).date.strftime('%Y-%m-%d')}")
print(f"\nMood distribution:")
for mood, count in mood_counts.items():
percentage = (count / total_entries) * 100
print(f" {mood}: {count} ({percentage:.1f}%)")
def main():
diary = PersonalDiary()
while True:
print("\n" + "="*40)
print("PERSONAL DIARY APPLICATION")
print("="*40)
print("1. Add new entry")
print("2. View all entries")
print("3. Search entries")
print("4. Filter by mood")
print("5. View statistics")
print("6. Exit")
choice = ask("\nSelect an option (1-6): ", '6').strip()
if choice == '1':
print("\nAdding new diary entry:")
title = ask("Enter title: ", '6').strip()
print("Enter content (press Enter twice to finish):")
content_lines = []
while True:
line = ask("", '6')
if line == "":
break
content_lines.append(line)
content = "\n".join(content_lines)
print("Select mood:")
print("1. Happy 2. Sad 3. Excited 4. Anxious 5. Peaceful 6. Other")
mood_choice = ask("Enter choice (1-6): ", '6').strip()
mood_map = {'1': 'happy', '2': 'sad', '3': 'excited',
'4': 'anxious', '5': 'peaceful', '6': 'other'}
mood = mood_map.get(mood_choice, 'neutral')
if mood == 'other':
mood = ask("Enter custom mood: ", '6').strip() or 'neutral'
diary.add_entry(title, content, mood)
elif choice == '2':
diary.view_entries()
elif choice == '3':
keyword = ask("Enter search keyword: ", 'python').strip()
if keyword:
diary.search_entries(keyword)
elif choice == '4':
mood = ask("Enter mood to filter by: ", '6').strip()
if mood:
diary.filter_by_mood(mood)
elif choice == '5':
diary.get_statistics()
elif choice == '6':
print("Thank you for using Personal Diary. Goodbye!")
break
else:
print("Invalid choice. Please try again.")
if __name__ == "__main__":
main() Run it
Section titled “Run it”python personaldiary.py1. Add new entry
2. View all entries
3. Search entries
4. Filter by mood
5. View statistics
6. Exit
Choice: 1
Title: First entry
Mood (happy/sad/neutral/excited/anxious): happy
Content (end with empty line):
> Started my Python diary today.
>
Saved.What it produces
Section titled “What it produces”Running the file exactly as it ships takes 0.1 s and prints:
========================================
PERSONAL DIARY APPLICATION
========================================
1. Add new entry
2. View all entries
3. Search entries
4. Filter by mood
5. View statistics
6. Exit
Select an option (1-6): 6 (no input available, using the default)
Thank you for using Personal Diary. Goodbye!Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. The data model
Section titled “1. The data model”from dataclasses import dataclass, field, asdict
from datetime import datetime
@dataclass
class DiaryEntry:
title: str
content: str
mood: str = "neutral"
date: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
def to_dict(self):
return asdict(self)
@classmethod
def from_dict(cls, d):
return cls(**d)Why @dataclass? In one decorator you get:
- A real
__init__that accepts each field. - A
__repr__for clean printing. - Equality based on field values.
- Optional default values.
field(default_factory=...) is the right way to default a mutable or computed value — it runs each time a new entry is created, so every entry gets its own current timestamp.
2. The Diary class
Section titled “2. The Diary class”import json
from pathlib import Path
class Diary:
def __init__(self, filename="diary.json"):
self.filename = Path(filename)
self.entries: list[DiaryEntry] = []
self.load()
def load(self):
if self.filename.exists():
data = json.loads(self.filename.read_text(encoding="utf-8"))
self.entries = [DiaryEntry.from_dict(d) for d in data]
def save(self):
data = [e.to_dict() for e in self.entries]
self.filename.write_text(json.dumps(data, indent=2), encoding="utf-8")
def add(self, entry: DiaryEntry):
self.entries.append(entry)
self.save()Reading and writing the whole file each time is fine for personal use — JSON is small and humans like being able to open it in a text editor. If you ever had millions of entries you would swap to SQLite.
3. Search and filter
Section titled “3. Search and filter”def search(self, keyword: str) -> list[DiaryEntry]:
k = keyword.lower()
return [e for e in self.entries
if k in e.title.lower() or k in e.content.lower()]
def by_mood(self, mood: str) -> list[DiaryEntry]:
return [e for e in self.entries if e.mood == mood]List comprehensions read like English. The lower() calls make search case-insensitive.
4. Statistics
Section titled “4. Statistics”from collections import Counter
def stats(self):
if not self.entries:
return {"total": 0, "moods": {}}
moods = Counter(e.mood for e in self.entries)
return {
"total": len(self.entries),
"moods": {m: f"{c} ({c/len(self.entries):.1%})"
for m, c in moods.most_common()},
"first": self.entries[0].date,
"latest": self.entries[-1].date,
}Counter is the right tool any time you find yourself reaching for dict plus +=.
5. The menu loop
Section titled “5. The menu loop”def main():
diary = Diary()
while True:
print("\n1. Add 2. View 3. Search 4. By mood 5. Stats 6. Exit")
choice = input("Choice: ").strip()
if choice == "1":
add_entry(diary)
elif choice == "2":
view_all(diary)
elif choice == "3":
search_entries(diary)
elif choice == "4":
filter_by_mood(diary)
elif choice == "5":
show_stats(diary)
elif choice == "6":
break
else:
print("Invalid choice.")Each menu option calls a separate helper function. Each helper does one thing. The main loop only routes.
6. Multi-line content
Section titled “6. Multi-line content”def read_multiline(prompt: str) -> str:
print(prompt + " (end with an empty line)")
lines = []
while True:
line = input("> ")
if line == "":
break
lines.append(line)
return "\n".join(lines)This pattern lets users write paragraphs, not just one line. Press Enter on an empty line to finish.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
json.decoder.JSONDecodeError on first run | File exists but is empty | Check if self.filename.exists() and self.filename.stat().st_size > 0 |
| Entries lost after restart | Forgot self.save() after adding | Save on every mutating operation, not just exit |
| Unicode errors | Default encoding mismatch | Always pass encoding="utf-8" to read/write |
KeyError after upgrading the schema | Old file lacks a new field | Use .get() in from_dict or run a one-shot migration |
| Search returns nothing | Case mismatch | .lower() both sides |
Variations to Try
Section titled “Variations to Try”1. Edit and delete entries
Section titled “1. Edit and delete entries”Number entries when displaying them; let the user type the number to edit or delete.
2. Encrypted entries
Section titled “2. Encrypted entries”A diary should be private. Use cryptography.fernet:
from cryptography.fernet import Fernet
key = Fernet(...).generate_key() # store separately!
cipher = Fernet(key)
encrypted = cipher.encrypt(content.encode())Save encrypted instead of the raw content. Decrypt on load. Better: derive the key from a user-provided password using PBKDF2HMAC.
3. Date filtering
Section titled “3. Date filtering”“Show entries from last week” or “from 2025-01-01 to 2025-01-31”.
from datetime import datetime
start = datetime.fromisoformat("2025-01-01")
end = datetime.fromisoformat("2025-01-31")
return [e for e in self.entries
if start <= datetime.fromisoformat(e.date) <= end]4. Tags / categories
Section titled “4. Tags / categories”Add a tags: list[str] field; let the user filter by tag the same way as by mood.
5. Sentiment analysis
Section titled “5. Sentiment analysis”Use TextBlob or VADER to compute a sentiment score per entry; correlate it with your self-reported mood.
6. Markdown rendering
Section titled “6. Markdown rendering”Save entries as Markdown. Export the diary as a single Markdown or HTML file you can print.
7. GUI version
Section titled “7. GUI version”Build a Tkinter UI with a list of dates on the left and the content on the right.
8. Cloud backup
Section titled “8. Cloud backup”Sync the diary.json file to S3 / Google Drive on save. Encrypt before upload.
9. Mood chart
Section titled “9. Mood chart”Use matplotlib to plot moods over time:
import matplotlib.pyplot as plt
dates = [e.date for e in diary.entries]
moods = [e.mood for e in diary.entries]
plt.plot(dates, moods, marker="o")
plt.xticks(rotation=45)
plt.tight_layout(); plt.show()10. Reminder
Section titled “10. Reminder”Schedule a daily prompt at 9 PM to write today’s entry. See Basic Alarm Clock for the scheduling idea.
Privacy Considerations
Section titled “Privacy Considerations”A diary on disk is plaintext by default. If you keep anything personal:
- Move the diary file out of any auto-synced folder (Dropbox, OneDrive, iCloud) unless those services are encrypted at rest with your key.
- Encrypt at rest with
cryptography.fernetand a password-derived key. - Never commit your diary file to Git. Add
diary.jsonto.gitignore. - Back up the file somewhere safe — losing it is permanent.
Real-World Applications
Section titled “Real-World Applications”- Personal journaling — what this is.
- Mood / habit tracking — bullet-journal-style logs.
- Reading log or workout log — same data structure, different fields.
- Customer-service ticketing prototypes — entries are tickets, moods are priorities.
- Bug-tracking for solo projects.
Educational Value
Section titled “Educational Value”- Data modeling with
@dataclass. - Persistence with JSON, including schema evolution.
- Searching and filtering with list comprehensions.
- Counting and aggregation with
Counter. - CLI design — clear menus, helper functions, no global state.
Next Steps
Section titled “Next Steps”- Build the encryption layer (
cryptography.fernet). - Add a edit/delete flow.
- Plot mood over time with matplotlib.
- Wrap with a Tkinter GUI or a Flask web app.
- Migrate storage from JSON to SQLite when you cross a few thousand entries.
Conclusion
Section titled “Conclusion”You built a real productivity tool: timestamped, searchable, statistically aware. The same architecture (data class + persistence + menu loop) underlies thousands of small CLI tools used in the wild. The full source is on GitHub. Explore more on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading