Learning Goals
3 minBy the end of this lesson you can:
- Build a multi-line f-string story template with several named slots.
- Collect user answers into a dict and reference them by key in the f-string.
- Add tiny calculations and method calls (
.upper(),.lower()) inside braces for extra polish.
Warm-Up
5 minThis was your Level-1 Mad Libs core:
name = input("A name: ") adj = input("An adjective: ") verb = input("A verb: ") print("One day, " + name + " went to the " + adj + " forest to " + verb + ".")
It works, but reading it is hard. The shape of the story is buried in the +s. Now look at the f-string version:
print(f"One day, {name} went to the {adj} forest to {verb}.")
Compare
Same output, but the f-string reads exactly like the story — with the user's words in slots. That clarity is what makes the rebuild worth doing.
Mad Libs is just a template plus user inputs. f-strings make the template look like the finished story — which is exactly what you want when you're writing one.
New Concept · Story Templates
14 minStep 1 · Collect the words
Ask for each word with input(). We'll store them in a dictionary so the slots have names — easier to reference and to swap stories later.
words = { "name" : input("A friend's name: "), "adj" : input("An adjective: "), "noun" : input("A noun (a thing): "), "verb" : input("A verb ending in -ing: "), "place" : input("A place: "), "number" : input("A number: "), }
One dict, one entry per slot. Easy to extend with more slots later, easy to print all the user's answers back for laughs.
Step 2 · The story template
Write the whole story as a triple-quoted f-string. The slot names match the dict keys.
story = f""" One sunny day, {words["name"]} was walking through {words["place"]} when they spotted a very {words["adj"]} {words["noun"]}. The {words["noun"]} was {words["verb"]} like nothing they had ever seen! {words["name"]} stared for {words["number"]} whole minutes before deciding to write a blog post about it. THE END. """ print(story)
Step 3 · Tiny in-brace flourishes
Add small touches inside the braces — they don't need new variables.
story = f""" === {words["name"].upper()}'s ADVENTURE === One sunny day, {words["name"]} was walking through {words["place"]} when they spotted a very {words["adj"]} {words["noun"]}. The {words["noun"]} was {words["verb"]} like nothing they had ever seen! Then the {words["adj"].lower()} {words["noun"]} multiplied — within an hour there were {int(words["number"]) * 5} of them. Chaos. THE END. """
Three tiny tricks: .upper() on the title, .lower() to keep tone consistent inside the body, and int(words["number"]) * 5 to do maths on the user's number (remember input hands you a string).
The outer f-string uses double quotes ("""..."""), so when you dig into the dict inside a brace you must use single quotes: {words['name']} — or the parser gets confused. Some editors let you nest matching quotes from Python 3.12+, but the alternation is the safe habit.
Step 4 · Replay value with a function
Wrap the template into a function. Same template, any dict in.
def tell_story(w): return f""" === {w["name"].upper()}'s ADVENTURE === One sunny day, {w["name"]} walked through {w["place"]} ... """ print(tell_story(words)) print(tell_story({"name": "Aisyah", "place": "the moon", ...}))
This little move is the difference between a one-shot game and a Mad Libs engine. Same template, any cast of words.
Worked Example · The Mad Libs 2.0 Engine
12 minThe story
Build a full Mad Libs program that asks for six words, prints the story, then offers to play again.
Save as madlibs2.py:
Code
# madlibs2.py — Mad Libs with a dict + multi-line f-string template def ask_for_words(): return { "name" : input("A friend's name : "), "place" : input("A place : "), "adj" : input("An adjective : "), "noun" : input("A noun (a thing): "), "verb" : input("A verb -ing form: "), "number" : input("A number : "), } def tell_story(w): return f""" === {w["name"].upper()}'s ADVENTURE === One sunny day, {w["name"]} was walking through {w["place"]} when they spotted a very {w["adj"]} {w["noun"]}. The {w["noun"]} was {w["verb"]} like nothing they had ever seen! {w["name"]} stared for {w["number"]} whole minutes before realising the {w["adj"].lower()} {w["noun"]} multiplied — within an hour there were {int(w["number"]) * 5} of them. Chaos! THE END. """ while True: words = ask_for_words() print(tell_story(words)) again = input("Another round? (y/n) ") if again.lower() != "y": print("Thanks for playing!") break
Sample output
A friend's name : Aisyah A place : the school canteen An adjective : sparkly A noun (a thing): durian A verb -ing form: dancing A number : 3 === AISYAH'S ADVENTURE === One sunny day, Aisyah was walking through the school canteen when they spotted a very sparkly durian. The durian was dancing like nothing they had ever seen! Aisyah stared for 3 whole minutes before realising the sparkly durian multiplied — within an hour there were 15 of them. Chaos! THE END. Another round? (y/n) n Thanks for playing!
Read the diff
Three things to spot. (1) The ask_for_words function returns a dict — you can swap it for a different question set later (a pirate story, a sports story) without touching the template. (2) The template is wrapped in a function — the same template can be called many times. (3) The little int(w["number"]) * 5 handles the "input gives me a string" conversion right inside the brace.
Try It Yourself
13 minUse one triple-quoted f-string to print a two-line greeting that uses three values: name, age, hometown.
Hint
name = "Wei Jie" age = 13 home = "Penang" print(f""" Salam {name}! You are {age} years old and from {home}. """)
Write a tell_joke(setup, punch) function that returns a two-line f-string joke. Call it twice with different jokes and print both.
Hint
def tell_joke(setup, punch): return f""" Q: {setup} A: {punch} """ print(tell_joke("Why did the python cross the road?", "To get to the other site.")) print(tell_joke("What did the dict say to the list?", "Stop putting words in my mouth!"))
Function arguments fill the template — exactly the pattern from the worked example.
Build a second story template — tell_pirate(w). Ask the user at the start to pick story 1 (adventure) or 2 (pirate), then call the right one.
Hint
def tell_pirate(w): return f""" Yarr! Cap'n {w["name"]} sailed to {w["place"]} in search of a {w["adj"]} {w["noun"]}. He {w["verb"]} for {w["number"]} days before the crew mutinied. Aye, the end. """ pick = input("Story (1 adventure, 2 pirate): ") words = ask_for_words() if pick == "2": print(tell_pirate(words)) else: print(tell_story(words))
Same set of input slots; two different output templates. That's the whole point of templating — swap the story without changing the questions.
Mini-Challenge · The Three-Story Engine
8 minBuild madlibs3.py — an engine with three story templates and a menu. The user picks a story, the engine asks for the right words, the story prints.
Your file must:
- Define three template functions:
tell_school(w),tell_pirate(w),tell_alien(w). Each takes a dict and returns a triple-quoted f-string. - Define
ask_for_words()that returns a dict with six slots, all shared between stories:name,place,adj,noun,verb,number. - In a
while True:loop, print a menu, accept1/2/3/q, collect words, print the chosen story. - At the end of each story, print a short stats line like
(150 characters, 28 words)— usinglen(story)andlen(story.split()).
Stretch goal. Pick the story randomly when the user types r — use random.choice([tell_school, tell_pirate, tell_alien])(words).
Show one possible solution
# madlibs3.py — three-story engine import random def ask_for_words(): return { "name" : input("Name : "), "place" : input("Place : "), "adj" : input("Adjective : "), "noun" : input("Noun : "), "verb" : input("Verb -ing : "), "number" : input("Number : "), } def tell_school(w): return f""" === SCHOOL DAYS === {w["name"]} arrived at {w["place"]} only to find a {w["adj"]} {w["noun"]} {w["verb"]} on the teacher's desk. After {w["number"]} attempts to catch it, the bell rang — the end. """ def tell_pirate(w): return f""" === YARRR === Cap'n {w["name"]} sailed to {w["place"]} hunting the {w["adj"]} {w["noun"]}. The crew were {w["verb"]} for {w["number"]} long days. Mutiny ensued. """ def tell_alien(w): return f""" === FIRST CONTACT === The {w["adj"]} alien landed in {w["place"]} carrying a {w["noun"]} that kept {w["verb"]}. It greeted {w["name"]} by raising {w["number"]} tentacles. """ while True: print() print("1 school 2 pirate 3 alien r random q quit") pick = input("Story: ").lower() if pick == "q": break words = ask_for_words() if pick == "1": story = tell_school(words) elif pick == "2": story = tell_pirate(words) elif pick == "3": story = tell_alien(words) elif pick == "r": story = random.choice([tell_school, tell_pirate, tell_alien])(words) else: print("Pick 1, 2, 3, r or q.") continue print(story) print(f"( {len(story)} characters, {len(story.split())} words )")
Non-negotiables: three template functions, one shared question collector, a menu loop, and a stats line using len(story) and len(story.split()). The random.choice([...])(words) trick picks a function from a list and calls it with the dict — two operations in one line.
Recap
3 minMad Libs 2.0 is the simplest possible template engine. The story is one multi-line f-string with named slots. The slots are filled from a dictionary. The whole story is a function, so you can swap templates without changing the question logic. With three lines of code per story you can host as many stories as you can write — switchable by a menu or chosen at random.
Vocabulary Card
- template
- A string with named slots that get filled in. An f-string is a template; the braces are the slots.
- slot
- A single
{key}placeholder inside the template. - quote alternation
- Using single quotes inside a double-quoted f-string (or vice versa) so the parser doesn't get confused.
Homework
4 minSave my_madlib.py. Write your own Mad Libs story — at least eight lines long, at least seven slots. Topic of your choice (sports match, magic spell, hawker stall, anything).
Your file must:
- Ask the user for at least seven words. Use a dictionary.
- Print the story as one triple-quoted f-string.
- Use at least one
.upper()/.lower()inside a brace. - Use at least one inline calculation — for example
int(words["number"]) * 2. - End with the stats line:
( N characters, M words ).
Bring it to class — best stories get read out loud.
Sample · my_madlib.py
# my_madlib.py — football-match Mad Lib w = { "team_a" : input("Your team's name: "), "team_b" : input("Other team's name: "), "player" : input("A player's name: "), "adj" : input("An adjective: "), "verb" : input("A verb (past tense): "), "noun" : input("A noun (a thing): "), "goals" : input("How many goals? "), } story = f""" === MATCH REPORT: {w["team_a"].upper()} vs {w["team_b"].upper()} === It was a {w["adj"]} afternoon at the stadium when {w["player"]} {w["verb"]} the {w["noun"]} into the back of the net. By full time, {w["team_a"]} had scored {w["goals"]} goals — twice that many ({int(w["goals"]) * 2}) if you count the shots that hit the bar. {w["team_b"]} went home empty-handed. THE END. """ print(story) print(f"( {len(story)} characters, {len(story.split())} words )")
Non-negotiables: seven inputs into a dict, one triple-quoted f-string, at least one .upper(), one inline calculation, and the stats line at the end. Your story can be about anything.