A Python script that generates personalized letters by replacing a [name] placeholder in a template for each recipient in a list.
- Quick start
- Builds comparison
- Usage
- Data flow
- Features
- Navigation flow
- Architecture
- Module reference
- Configuration reference
- Data schema
- Design decisions
- Course context
- Dependencies
git clone https://github.com/xavier-oc-programming/mail-merge-python
cd mail-merge-python
python menu.pySelect 1 for the original course build or 2 for the advanced OOP build.
To run a build directly:
python original/main.py
python advanced/main.py| Feature | Original | Advanced |
|---|---|---|
| Reads template file | ✓ | ✓ |
| Reads names list | ✓ | ✓ |
Replaces [name] placeholder |
✓ | ✓ |
| Generates one file per name | ✓ | ✓ |
| Skips blank lines in names file | ✗ | ✓ |
Path-based portable file paths |
✓ | ✓ |
MailMerger class (OOP) |
✗ | ✓ |
Constants centralized in config.py |
✗ | ✓ |
Pure-logic generate() (no file I/O) |
✗ | ✓ |
| Completion summary with letter count | ✗ | ✓ |
No arguments or environment variables required. Both builds run silently and print a completion message on finish.
Mail merge complete! 8 letters saved to advanced/output/
Input: starting_letter.txt Input: invited_names.txt
(template with (one name per line)
[name] placeholder)
| |
+---------------+---------------+
|
v
For each name:
- strip whitespace
- replace [name] in template
- write letter_for_{name}
|
v
Print completion summary
At each stage:
- Load:
str(raw template text) andlist[str](name strings) - Process: one
strper recipient afterreplace()andstrip() - Output: plain-text files named
letter_for_{name}in the output directory
Template substitution — A single [name] token in the template is replaced with each recipient's name, producing one personalized letter per person.
Name list reading — The names file is read line by line. The advanced build additionally strips whitespace and skips blank lines, making it robust to trailing newlines or empty lines in the names file.
File output — Each letter is written to advanced/output/ (or original/Output/ReadyToSend/) with the filename letter_for_{name}. Generated files are git-ignored; example.txt is committed to document the expected format.
Completion summary (advanced only) — Prints the count of letters generated and the output path.
Pure-logic generate() method (advanced only) — Accepts a template string and a name, returns the personalized letter string. No file I/O, no side effects — independently testable without touching the filesystem.
Centralized constants (advanced only) — All file paths and string tokens are defined once in config.py. Changing the template location, output directory, or placeholder token requires editing one file.
python menu.py
|
v
+---------------------------+
| MAIL MERGE (menu) |<-----------------------+
+---------------------------+ |
| 1 -> original build | |
| 2 -> advanced build | |
| q -> quit | |
+-------------+-------------+ |
| |
+---------+----------+----------+ |
| | | |
v v v |
"1" "2" other input |
| | | |
v v v |
original/ advanced/ "Invalid choice. |
main.py main.py Try again." |
| | | |
+---------+ +----------------+
|
v
script completes
"Press Enter to return..."
|
+--------------------------------------------->(loop)
+------------------------------+
| Load config | TEMPLATE_PATH, NAMES_PATH, OUTPUT_DIR
+---------------+--------------+
|
v
+------------------------------+
| Read template | starting_letter.txt -> str
+---------------+--------------+
|
v
+------------------------------+
| Read names | invited_names.txt -> list[str]
+---------------+--------------+
|
v
+------------------------------+
| For each name: |
| strip whitespace |
| replace [name] in |
| template |
| write letter_for_{name} |
+---------------+--------------+
|
v
+------------------------------+
| Print completion summary |
+------------------------------+
mail-merge-python/
|-- menu.py # CLI launcher -- prompts for build selection
|-- art.py # LOGO constant displayed by menu.py
|-- requirements.txt # stdlib only; no pip install required
|-- .gitignore
|-- README.md
|-- docs/
| `-- COURSE_NOTES.md # original course exercise description
|-- original/ # verbatim course solution (branch: original)
| |-- main.py # procedural; only change: Path-based file paths
| |-- Input/
| | |-- Letters/
| | | `-- starting_letter.txt
| | `-- Names/
| | `-- invited_names.txt
| `-- Output/
| `-- ReadyToSend/ # generated letters (git-ignored); example.txt committed
`-- advanced/ # OOP refactor
|-- config.py # all constants -- paths, placeholder, prefix
|-- merger.py # class MailMerger -- pure logic, no I/O side effects
|-- main.py # orchestrator -- wires config, MailMerger, and file output
|-- input/
| |-- Letters/
| | `-- starting_letter.txt
| `-- Names/
| `-- invited_names.txt
`-- output/ # generated letters (git-ignored); example.txt committed
`-- example.txt
| Method | Returns | Description |
|---|---|---|
__init__(template_path, names_path) |
— | Stores Path objects for the template and names files |
load_template() |
str |
Reads and returns the full template text |
load_names() |
list[str] |
Reads names file; strips whitespace and drops blank lines |
generate(template, name) |
str |
Replaces NAME_PLACEHOLDER with name in the template string |
| Constant | Default | Description |
|---|---|---|
BASE_DIR |
Path(__file__).parent |
Root of the advanced/ directory |
TEMPLATE_PATH |
BASE_DIR / "input/Letters/starting_letter.txt" |
Letter template with [name] placeholder |
NAMES_PATH |
BASE_DIR / "input/Names/invited_names.txt" |
Recipient names, one per line |
OUTPUT_DIR |
BASE_DIR / "output" |
Directory where personalized letters are written |
NAME_PLACEHOLDER |
"[name]" |
Token in the template replaced per recipient |
OUTPUT_FILE_PREFIX |
"letter_for_" |
Prepended to each name to form the output filename |
Plain text. Contains exactly one [name] token.
Dear [name],
You are invited to my birthday this Saturday.
Hope you can make it!
Angela
Plain text. One name per line. Blank lines are ignored by the advanced build.
Aang
Zuko
Appa
Katara
Sokka
Momo
Uncle Iroh
Toph
Plain text. One file per recipient. Identical to the template with [name] replaced.
Dear Aang,
You are invited to my birthday this Saturday.
Hope you can make it!
Angela
config.py centralizes all paths and tokens — changing the input folder, output folder, or placeholder token requires editing one file. No magic strings scattered across modules.
MailMerger.generate() is pure logic — it accepts a template string and a name, returns a string. No file I/O, no side effects. This means it can be tested without touching the filesystem: pass any string in, assert the output.
load_names() strips and filters — strip() removes leading/trailing whitespace and newlines. Blank lines are dropped. This makes the script robust to editors that append a trailing newline to the names file.
Path(__file__).parent for all file paths — all paths are resolved relative to the script's own location, not the working directory. Both python original/main.py and menu.py → 1 work correctly regardless of where the interpreter is invoked from.
Pure-logic modules raise exceptions, not sys.exit() — MailMerger raises FileNotFoundError if a path is missing. main.py decides how to handle it. This keeps modules reusable and independently testable.
sys.path.insert in advanced/main.py — inserting Path(__file__).parent at the front of sys.path ensures sibling imports (from config import ..., from merger import ...) resolve correctly whether launched via menu.py or run directly.
subprocess.run with cwd=path.parent in menu.py — sets the working directory to the build's own folder before launching. This ensures any relative-path code in the build resolves correctly from the right location.
while True in menu.py, not recursion — after a build finishes, control returns to the loop naturally. Recursion would grow the call stack on every build run; a loop does not.
Console cleared before every menu render — os.system("clear") at the top of each loop iteration keeps the menu clean after a build completes and prints its output.
Built as Day 24 of 100 Days of Code: The Complete Python Pro Bootcamp by Dr. Angela Yu.
Concepts covered in the original build: file I/O (open, read, readlines, write), string manipulation (replace, strip), f-strings, for loops over file lines.
The advanced build extends into: OOP (MailMerger class), separation of concerns (config / logic / orchestration), pathlib.Path, pure-function design.
See docs/COURSE_NOTES.md for the full original course exercise description.
| Module | Used in | Purpose |
|---|---|---|
os |
menu.py |
Clear the console with os.system |
sys |
menu.py, advanced/main.py |
sys.executable, sys.path.insert |
subprocess |
menu.py |
Launch builds in a child process |
pathlib.Path |
advanced/config.py, advanced/main.py, original/main.py |
Portable, manipulation-friendly file paths |