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 that ask for the missing words, and a printed final story. It is good practice for f-strings, dicts and reading template files.

What you'll practise: f-strings, format strings, string templates, dicts, regex extraction, file reading.


Step 1: The Simplest Version

The words and the template are written directly in the code.

python
# Simulated user input (in real code: input("Pick a noun: ") and so on)
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)

That's the simplest possible Mad Libs. Run it. Then change the words and run it again.


Step 2: Templates from a String

When the story is an f-string, the template is part of the code. To change the story, you must change the code. Let's separate them.

An f-string is filled in at the moment its line runs, with the variables that exist then. You can't store it as plain text and fill it in later. For that, we need str.format():

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)

**words unpacks the dict into keyword arguments, so the call becomes template.format(animal="platypus", adjective="fluffy", ...). This is shorter than passing each word by hand.


Step 3: Find the Blanks Automatically

How do we know which words to ask for? We read the placeholder names from 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. set() removes the duplicates.
placeholders = set(re.findall(r"\{(\w+)\}", template))
print(placeholders)   # {'adjective', 'animal', 'verb', 'place'}

The regex \{(\w+)\} matches a name made of letters, digits or underscores inside curly braces, like {animal}, and captures the name.


Step 4: Prompt for Each Blank

Now put the steps together: find the blanks, ask for each word, fill in the template.

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)
# dict.fromkeys removes duplicates AND keeps the order of first appearance
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). It removes duplicates and keeps the order in which each name first appears. A set also removes duplicates, but it doesn't keep the order. (Here the order decides which word the player is asked for first.)


Step 5: Templates in Files

Templates are data, not code. They should be stored in files, so that people who don't program 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))

Anyone can now add a new template by saving a file such as 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: use random.choice(list(TEMPLATES.values())) to pick a template the user doesn't expect.
2. Hints for tricky blanks: {adjective} is obvious, but {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 the game with Flask and get the words from an HTML form. (Try this after the web lessons.)


🎯 Your Turn: Add Hint Prompts

When the template has {exclamation}, the word "exclamation" alone may not be clear to the player. Add a HINTS dict so that each prompt can explain more: "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 (for example, park)",
    "relative": "a family member",
    "adjective": "an adjective (for example, spectacular)",
    "animal": "an animal",
    "verb": "a verb ending in -ing",
    "object": "an object",
    "exclamation": "an exclamation (for example, '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: Removing duplicates and keeping the order list(dict.fromkeys(...)) removes duplicates from a list and keeps the order in which items first appear.
Hint 2: Building the result dict {name: hints.get(name, name) for name in placeholders} is a dict comprehension. If a name has no hint, hints.get returns the name itself.
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) to fill a template that is stored as data (and how it differs from f-strings)
  • Dict unpacking with **
  • re.findall(r"\{(\w+)\}", text) to extract placeholders
  • dict.fromkeys(seq) to remove duplicates and keep the order
  • Templates as data (separate from the code that uses them)

Keeping templates in one file and code in another is a common design. Template engines such as Jinja2 and Django templates work the same way, on a larger scale.

Next: Personal Journal, a tool you can keep using.

Found this useful? Share it with a friend who’s learning Python.