Project: Mad Libs Generator
1 · The lesson
readYou'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.
# 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():
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.
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.
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.
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
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')".
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
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 placeholdersdict.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.