Project: Mad Libs Generator
1 · The lesson
readYou'll build the classic word game: a template with blanks ("My favourite ___ is ___"), prompts for filling them, and a printed final story. It's a perfect playground for f-strings, dicts, and reading template files.
What you'll practice: f-strings, format strings, string templates, dicts, regex extraction, file reading.
Step 1 — The Bare-Bones Version
Hardcoded prompts and a template.
# Simulated user input (in real code: input("Pick a noun: ") etc.) animal = "platypus" adjective = "fluffy" verb = "skateboarding" place = "Antarctica" story = f""" Once upon a time, a {adjective} {animal} decided to go {verb} in {place}. The locals were confused. The {animal} just shrugged and kept going. """ print(story)
Done — the simplest possible Mad Libs. Run it. Change the inputs, run again.
Step 2 — Templates from a String
Hardcoding the f-string couples the template to the code. Let's separate them.
Python's f-strings can't be data (they're evaluated at parse time). We need str.format() instead:
template = """ Once upon a time, a {adjective} {animal} decided to go {verb} in {place}. The locals were confused. The {animal} just shrugged and kept going. """ words = { "animal": "platypus", "adjective": "fluffy", "verb": "skateboarding", "place": "Antarctica", } story = template.format(**words) print(story)
The **words unpacks the dict into keyword arguments — so template.format(animal="platypus", adjective="fluffy", ...). Cleaner than passing each by hand.
Step 3 — Auto-Discover the Blanks
How do we know which words to prompt for? Parse them out of the template.
import re template = """ Once upon a time, a {adjective} {animal} decided to go {verb} in {place}. The locals were confused. The {animal} just shrugged and kept going. """ # Find every {word} placeholder — use set() to dedupe placeholders = set(re.findall(r"\{(\w+)\}", template)) print(placeholders) # {'adjective', 'animal', 'verb', 'place'}
The regex \{(\w+)\} matches {anything-wordy} and captures the name inside.
Step 4 — Prompt for Each Blank
Combine: parse → prompt → fill.
import re template = """ Today I went to the {place} with my {relative}. We saw a {adjective} {animal} {verb} a {object}! {relative} said, "{exclamation}!" We laughed all the way home. """ placeholders = re.findall(r"\{(\w+)\}", template) # Use dict.fromkeys to preserve first-seen order AND dedupe unique_blanks = list(dict.fromkeys(placeholders)) # Simulated answers (in real code: input(f"Give me a(n) {blank}: ") for each) fake_inputs = { "place": "library", "relative": "grandma", "adjective": "majestic", "animal": "alpaca", "verb": "juggling", "object": "rubber duck", "exclamation": "Holy moly", } # In a real terminal: # words = {} # for blank in unique_blanks: # words[blank] = input(f"Give me a(n) {blank}: ") words = fake_inputs print(template.format(**words))
Notice dict.fromkeys(placeholders) — a quick way to dedupe while keeping first-seen order. Sets dedupe but don't preserve order. (Order isn't strictly required here, but the principle is useful.)
Step 5 — Templates in Files
Templates ARE data. They should live in files, so non-programmers can write new ones.
import re import io # In real code: # with open("templates/library.txt", encoding="utf-8") as f: # template = f.read() # In the browser, simulate with a string buffer: template_file = io.StringIO("""Today I went to the {place} with my {relative}. We saw a {adjective} {animal} {verb} a {object}! {relative} said, "{exclamation}!" """) template = template_file.read() placeholders = list(dict.fromkeys(re.findall(r"\{(\w+)\}", template))) words = { "place": "park", "relative": "uncle", "adjective": "dignified", "animal": "llama", "verb": "balancing", "object": "stack of pancakes", "exclamation": "Sweet biscuits", } print(template.format(**words))
A non-coder can now add a new template by saving templates/whatever.txt. The code doesn't change.
Step 6 — Polished Final Version
import re import random # A few templates as strings (would be in separate files in real code) TEMPLATES = { "library": """Today I went to the {place} with my {relative}. We saw a {adjective} {animal} {verb} a {object}! {relative} said, "{exclamation}!" We laughed all the way home.""", "cooking": """My {adjective} {relative} taught me how to make {food}. First, you take a {object} and {verb} it. Then add three {adjective2} {ingredient}s. Serve it on a {object2} at {temperature} degrees.""", } def get_placeholders(template): """Extract unique placeholder names in first-seen order.""" return list(dict.fromkeys(re.findall(r"\{(\w+)\}", template))) def fill_template(template, answers): return template.format(**answers) def play(template_name, answers): template = TEMPLATES[template_name] placeholders = get_placeholders(template) missing = set(placeholders) - set(answers) if missing: raise ValueError(f"Missing answers for: {sorted(missing)}") return fill_template(template, answers) # Demo sample_answers = { "place": "library", "relative": "grandma", "adjective": "splendid", "animal": "alpaca", "verb": "tap-dancing on", "object": "stapler", "exclamation": "Goodness gracious", } print(play("library", sample_answers))
Stretch Goals
1. Random templates: random.choice(list(TEMPLATES.values())) — surprise the user.
2. Hints for tricky blanks: {adjective} is obvious; {exclamation} may not be. Add a hints dict: {"exclamation": "something you shout, like 'wow'"}.
3. Save the funniest results: write each filled story to madlibs_archive.txt.
4. Multi-line templates: support templates with chapters / sections.
5. Web version: serve via Flask, get input via HTML form. (Comes after the web lessons.)
🎯 Your Turn — Add Hint Prompts
When {exclamation} shows up, "exclamation" alone isn't obvious. Add a HINTS dict so the prompts can be richer: "give me an exclamation (something you shout, like 'wow')".
import re TEMPLATE = """Today I went to the {place} with my {relative}. We saw a {adjective} {animal} {verb} a {object}! {relative} said, "{exclamation}!" """ HINTS = { "place": "a place (e.g. park)", "relative": "a family member", "adjective": "an adjective (e.g. spectacular)", "animal": "an animal", "verb": "a verb ending in -ing", "object": "an object", "exclamation": "an exclamation (e.g. 'Holy cow')", } def prompt_for(template, hints): """Find each placeholder, return a dict {name: hint_text}. The user can then loop over this dict to gather inputs. """ # TODO 1: extract unique placeholders from the template (preserve order) # TODO 2: build a dict mapping each placeholder to its hint # (fallback: the placeholder name itself if no hint exists) pass # Test prompts = prompt_for(TEMPLATE, HINTS) for name, hint in prompts.items(): print(f" Give me {hint} ({name})")
Hint 1 — Order-preserving dedup
list(dict.fromkeys(...)) dedupes a list while preserving first-seen order.
Hint 2 — Building the result dict
{name: hints.get(name, name) for name in placeholders} — dict comprehension with a default.
Show full solution
import re def prompt_for(template, hints): placeholders = list(dict.fromkeys(re.findall(r"\{(\w+)\}", template))) return {name: hints.get(name, name) for name in placeholders} prompts = prompt_for(TEMPLATE, HINTS) for name, hint in prompts.items(): print(f" Give me {hint} → ({name})") # Real version with input(): # answers = {name: input(f"Give me {hint}: ") for name, hint in prompts.items()} # print(TEMPLATE.format(**answers))
setup added so this can run · defines TEMPLATE, HINTS
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) TEMPLATE = _AutoMock('TEMPLATE') HINTS = _AutoMock('HINTS')
What You Learned
str.format(**dict)— the templated-string pattern (and why it's different from f-strings)- Dict unpacking with
** re.findall(r"\{(\w+)\}", text)to extract placeholdersdict.fromkeys(seq)to dedupe while preserving order- Templates as data (separate from the code that uses them)
This separation — templates in one file, code in another — is the foundation of every web framework (Jinja2, Django templates) and every report generator. You just built the toy version of all of them.
Next: Personal Journal — a useful tool you'll keep.