PythonMastery
beginner 15 min read · lesson 6 of 15 in Projects

Project: Mad Libs Generator

1 · The lesson

read

You'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.

python
# 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:

python
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.

python
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.

python
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.

python
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

python
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')".

python
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
python
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 placeholders
  • dict.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.