Personal Diary Application
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
@dataclass@dataclasscuts that boilerplate). - How to read and write JSON with
json.dumpjson.dump/json.loadjson.load. - How to work with timestamps via
datetimedatetime. - How to build a clean, menu-driven CLI without spaghetti.
- How to add real privacy with optional encryption.
Prerequisites
- Python 3.6 or above (3.7+ recommended for
dataclassesdataclasses). - A text editor or IDE.
- Comfort with functions, lists, and dictionaries.
- Familiarity with running a
.py.pyfile from the terminal.
Concepts You Will Use
| Concept | Purpose |
|---|---|
| Class | Group related data (entry fields) and behavior. |
@dataclass@dataclass | Auto-generate __init____init__, __repr____repr__, comparison — less boilerplate. |
json.dumpjson.dump / json.loadjson.load | Persist Python objects to a text file as JSON. |
datetimedatetime | Timestamp entries with date and time. |
PathPath / os.path.existsos.path.exists | Detect whether the diary file is new or existing. |
| List comprehensions | Filter and search entries cleanly. |
Getting Started
Create the project
- Create a folder named
personal-diarypersonal-diary. - Inside it, create
personaldiary.pypersonaldiary.py. - (Optional) create
diary.jsondiary.json— the script will create it for you on first save.
Write the code
Personal Diary
Source# Personal Diary Application
import datetime
import os
import json
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 = input("\nSelect an option (1-6): ").strip()
if choice == '1':
print("\nAdding new diary entry:")
title = input("Enter title: ").strip()
print("Enter content (press Enter twice to finish):")
content_lines = []
while True:
line = input()
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 = input("Enter choice (1-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 = input("Enter custom mood: ").strip() or 'neutral'
diary.add_entry(title, content, mood)
elif choice == '2':
diary.view_entries()
elif choice == '3':
keyword = input("Enter search keyword: ").strip()
if keyword:
diary.search_entries(keyword)
elif choice == '4':
mood = input("Enter mood to filter by: ").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()
# Personal Diary Application
import datetime
import os
import json
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 = input("\nSelect an option (1-6): ").strip()
if choice == '1':
print("\nAdding new diary entry:")
title = input("Enter title: ").strip()
print("Enter content (press Enter twice to finish):")
content_lines = []
while True:
line = input()
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 = input("Enter choice (1-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 = input("Enter custom mood: ").strip() or 'neutral'
diary.add_entry(title, content, mood)
elif choice == '2':
diary.view_entries()
elif choice == '3':
keyword = input("Enter search keyword: ").strip()
if keyword:
diary.search_entries(keyword)
elif choice == '4':
mood = input("Enter mood to filter by: ").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
python personaldiary.pypython 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.1. 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.Step-by-Step Explanation
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)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@dataclass? In one decorator you get:
- A real
__init____init__that accepts each field. - A
__repr____repr__for clean printing. - Equality based on field values.
- Optional default values.
field(default_factory=...)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
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()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
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]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()lower() calls make search case-insensitive.
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,
}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,
}CounterCounter is the right tool any time you find yourself reaching for dictdict plus +=+=.
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.")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
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)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
| Problem | Cause | Fix |
|---|---|---|
json.decoder.JSONDecodeErrorjson.decoder.JSONDecodeError on first run | File exists but is empty | Check if self.filename.exists() and self.filename.stat().st_size > 0if self.filename.exists() and self.filename.stat().st_size > 0 |
| Entries lost after restart | Forgot self.save()self.save() after adding | Save on every mutating operation, not just exit |
| Unicode errors | Default encoding mismatch | Always pass encoding="utf-8"encoding="utf-8" to read/write |
KeyErrorKeyError after upgrading the schema | Old file lacks a new field | Use .get().get() in from_dictfrom_dict or run a one-shot migration |
| Search returns nothing | Case mismatch | .lower().lower() both sides |
Variations to Try
1. Edit and delete entries
Number entries when displaying them; let the user type the number to edit or delete.
2. Encrypted entries
A diary should be private. Use cryptography.fernetcryptography.fernet:
from cryptography.fernet import Fernet
key = Fernet(...).generate_key() # store separately!
cipher = Fernet(key)
encrypted = cipher.encrypt(content.encode())from cryptography.fernet import Fernet
key = Fernet(...).generate_key() # store separately!
cipher = Fernet(key)
encrypted = cipher.encrypt(content.encode())Save encryptedencrypted instead of the raw content. Decrypt on load. Better: derive the key from a user-provided password using PBKDF2HMACPBKDF2HMAC.
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]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
Add a tags: list[str]tags: list[str] field; let the user filter by tag the same way as by mood.
5. Sentiment analysis
Use TextBlob or VADER to compute a sentiment score per entry; correlate it with your self-reported mood.
6. Markdown rendering
Save entries as Markdown. Export the diary as a single Markdown or HTML file you can print.
7. GUI version
Build a Tkinter UI with a list of dates on the left and the content on the right.
8. Cloud backup
Sync the diary.jsondiary.json file to S3 / Google Drive on save. Encrypt before upload.
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()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
Schedule a daily prompt at 9 PM to write today’s entry. See Basic Alarm Clock for the scheduling idea.
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.fernetcryptography.fernetand a password-derived key. - Never commit your diary file to Git. Add
diary.jsondiary.jsonto.gitignore.gitignore. - Back up the file somewhere safe — losing it is permanent.
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
- Data modeling with
@dataclass@dataclass. - Persistence with JSON, including schema evolution.
- Searching and filtering with list comprehensions.
- Counting and aggregation with
CounterCounter. - CLI design — clear menus, helper functions, no global state.
Next Steps
- Build the encryption layer (
cryptography.fernetcryptography.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
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.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
