Python Madlibs Tutorial

by IoFAdmin at

python | programming

We're Going To Create Our Own Madlibs!

In this tutorial, we're going to make our own version of Madlibs using the Inflect library that we covered in a previous post and our own code.

Copyright Notice

I pulled Madlib templates from the Madlibz API so I'm guessing that they're handling any copyright issues or that they created the templates for public use.

You Don't Know What Madlibs Are?

In case you don't know what Madlibs are, they're very short fill in the blank stories that tell funny and crazy tales. The user doesn't know what their story will be when they provide a list of words. They're in for a silly surprise when the whole Madlib is revealed.

Enough Jibber Jabber! Let's Get To It

We'll focus on 3 parts of our project:

  • Madlibz Api: this is where we'll get our templates. This is optional if you want to create your own Madlibs templates from scratch.
  • Inflect library: this awesome library will help us handle verb and noun cases easily.
  • Our Madlibs Python class: this is where the magic happens.

Fetch My Template Please!

It's out of the scope of this tutorial for me to go into using an API in Python. If you want to request a few templates by hand, you can just go to the random generation page and save the responses in a text file.

Once you have your template(s) saved, you'll have to modify save them to a CSV file so that our Python code can use it. If you go to the random generation page, you'll get something back like this:

{
     "title": "Learning About History",
     "blanks": [
          "adjective",
          "noun",
          "nouns",
          "adjective",
          "nouns",
          "nouns",
          "animals",
          "nouns",
          "nouns",
          "number",
          "number",
          "nouns",
          "adjective",
          "nouns"
     ],
     "value": [
           "History is ",
           " because we learn about ",
           " and ",
           " that happened long ago. I can't believe people used to dress in ",
           " clothing and kids played with ",
           " and ",
           " instead of video games. Also, before cars were invented, people actually rode ",
           "! People read ",
           " instead of computers and tablets, and sent messages via ",
           " that took ",
           " days to arrive. I wonder how kids will view my life in ",
           " year(s); maybe they will ride flying cars to school and play with ",
           " and ",
           " ",
           "!",
           0
     ]
}

That's the data we need to build our data file. Create a new text file and name it templates.csv.

Set Up Our Templates File

If you use the data above, you can modify it to look like the the data below (or copy/paste). If you have something else, you'll have to modify it yourself. This data will end up in templates.csv

adjectives,nouns,numbers,colors,places,verbs,person,food,clothing,celebrity,occupation,text
3,9,3,0,0,0,0,0,0,0,0,"History is |ADJ| because we learn about |NOUN| and |PLURAL_NOUN| that happened long ago. I can't believe people used to dress in |ADJ| clothing and kids played with |PLURAL_NOUN| and |PLURAL_NOUN| instead of video games. Also, before cars were invented, people actually rode |PLURAL_NOUN|! People read |PLURAL_NOUN| instead of computers and tablets, and sent messages via |PLURAL_NOUN| that took |NUM| days to arrive. I wonder how kids will view my life in |NUM| year(s); maybe they will ride flying cars to school and play with |PLURAL_NOUN| and |ADJ| |PLURAL_NOUN|!"

That's our CSV (comma separated values) file. The first line of the file describes what the data is and all of the other lines (you can and should add more later) make up the actual data that our program will use. But what are |PLURAL_NOUN| and |ADJ|? I'm glad you asked, Random Internet Stranger. Anything between the two pipe characters are our placeholders that will be filled in with user-provider words. In other words, they're the blanks in our Madlibs. The numbers correspond to how many of the placeholder types our template has. In our example, we have 3 adjectives and 0 occupations.

Placeholders?

Our Madlibs clone allows for 11 types of placeholders: adjectives, nouns, numbers, colors, places, verbs, person, food, clothing, celebrity, and occupation. Their corresponding placeholders are: |ADJ|, |NOUN|, |PLURAL_NOUN|, |VERB|, |NUM|, |COLOR|, |PLACE|, |PERSON|, |FOOD|, |CLOTHING|, |CELEBRITY|, |OCCUPATION|. You may have noticed that we have two placeholders for nouns... we'll get to that later.

Madlibs class

This is the main part of our code. I'll post the code and then break it down.

import inflect
import csv
import random

class Madlibs:
    # madlib templates from http://madlibz.herokuapp.com/api/random
    listNames = ['adjective', 'noun', 'verb', 'number', 'color', 'place', 'person', 'food', 'clothing', 'celebrity', 'occupation']
    templatePlaceholders = {
        'adjective': '|ADJ|',
        'noun': '|NOUN|',
        'plural_noun': '|PLURAL_NOUN|',
        'verb': '|VERB|',
        'number': '|NUM|',
        'color': '|COLOR|',
        'place': '|PLACE|',
        'person': '|PERSON|',
        'food': '|FOOD|',
        'clothing': '|CLOTHING|',
        'celebrity': '|CELEBRITY|',
        'occupation': '|OCCUPATION|'
    }

    def __init__(self):
        self.inflector = inflect.engine()

    def setClassListByName(self, listName, data):
        try:
            if listName not in self.listNames:
                raise Exception(f'{listName} not in list of names')
            setattr(self, listName, data)
        except:
            print('Error!')

    def getClassListByName(self, listName):
        try:
            if listName not in self.listNames:
                raise Exception(f'{listName} not in list of names')
            return getattr(self, listName)
        except:
            pass

    def setSlug(self, slugType):
        return self.templatePlaceholders.get(slugType)

    def replaceCurrentSlug(self, replaceValue, slugType):
        self.template = self.template.replace(slugType, replaceValue.strip(), 1)

    def setTemplate(self, template):
        self.template = template

    def parseTemplates(self):
        self.templates = []

        with open('templates.csv') as csv_file:
            csv_reader = csv.reader(csv_file, delimiter=',')
            line_count = 0
            for row in csv_reader:
                if line_count == 0:
                    line_count += 1
                    continue

                self.templates.append({
                    'num_adjs' : row[0],
                    'num_nouns' : row[1],
                    'num_numbers' : row[2],
                    'num_colors' : row[3],
                    'num_places' : row[4],
                    'num_verbs' : row[5],
                    'num_persons' : row[6],
                    'num_foods' : row[7],
                    'num_clothes' : row[8],
                    'num_celebs' : row[9],
                    'num_occupations' : row[10],
                    'text' : row[11]
                })

        self.getInput(
            self.templates[ random.randint(0, len(self.templates) - 1) ]
        )

    def getTemplate(self):
        return self.template

    def fillNouns(self, nouns):
        for noun in nouns:
            next_noun_pos = self.template.find(self.setSlug('noun'))
            next_pluralnoun_pos = self.template.find(self.setSlug('plural_noun'))

            if next_noun_pos == -1 and next_pluralnoun_pos == -1:
                continue

            if next_noun_pos == -1:
                self.replaceCurrentSlug(self.inflector.plural(noun), self.setSlug('plural_noun'))
            else:
                self.replaceCurrentSlug(noun, self.setSlug('noun'))

    def fillTemplate(self):
        for listName in self.listNames:
            if self.getClassListByName(listName) is None:
                continue
            if listName == 'noun':
                self.fillNouns(self.getClassListByName('noun'))
                continue

            for ln in self.getClassListByName(listName):
                self.replaceCurrentSlug(ln, self.setSlug(listName))

    def getInput(self, templateDict):
        if int(templateDict.get('num_adjs')) > 0:
            self.setClassListByName('adjective', input(f"Enter {templateDict.get('num_adjs')} adjectives separated by commas: ").split(','))
        if int(templateDict.get('num_nouns')) > 0:
            self.setClassListByName('noun', input(f"Enter {templateDict.get('num_nouns')} nouns separated by commas: ").split(','))
        if int(templateDict.get('num_numbers')) > 0:
            self.setClassListByName('number', input(f"Enter {templateDict.get('num_numbers')} numbers separated by commas: ").split(','))
        if int(templateDict.get('num_colors')) > 0:
            self.setClassListByName('color', input(f"Enter {templateDict.get('num_colors')} colors separated by commas: ").split(','))
        if int(templateDict.get('num_places')) > 0:
            self.setClassListByName('place', input(f"Enter {templateDict.get('num_places')} places separated by commas: ").split(','))
        if int(templateDict.get('num_verbs')) > 0:
            self.setClassListByName('verb', input(f"Enter {templateDict.get('num_verbs')} verbs separated by commas: ").split(','))
        if int(templateDict.get('num_persons')) > 0:
            self.setClassListByName('person', input(f"Enter {templateDict.get('num_persons')} people separated by commas: ").split(','))
        if int(templateDict.get('num_foods')) > 0:
            self.setClassListByName('food', input(f"Enter {templateDict.get('num_foods')} foods separated by commas: ").split(','))
        if int(templateDict.get('num_clothes')) > 0:
            self.setClassListByName('clothing', input(f"Enter {templateDict.get('num_clothes')} articles of clothing separated by commas: ").split(','))
        if int(templateDict.get('num_celebs')) > 0:
            self.setClassListByName('celebrity', input(f"Enter {templateDict.get('num_celebs')} celebrities separated by commas: ").split(','))
        if int(templateDict.get('num_occupations')) > 0:
            self.setClassListByName('occupation', input(f"Enter {templateDict.get('num_occupations')} jobs separated by commas: ").split(','))

        self.setTemplate(templateDict.get('text'))

if __name__ == '__main__':
    madlib = Madlibs()

    madlib.parseTemplates()
    madlib.fillTemplate()

    print(madlib.getTemplate())

Explanation

def __init__(self):
    self.inflector = inflect.engine()

In the class constructor, we get an instance of the Inflector engine and save it to a class variable self.inflector.

def parseTemplates(self):
    self.templates = []

    with open('templates.csv') as csv_file:
        csv_reader = csv.reader(csv_file, delimiter=',')
        line_count = 0
        for row in csv_reader:
            if line_count == 0:
                line_count += 1
                continue

            self.templates.append({
                'num_adjs' : row[0],
                'num_nouns' : row[1],
                'num_numbers' : row[2],
                'num_colors' : row[3],
                'num_places' : row[4],
                'num_verbs' : row[5],
                'num_persons' : row[6],
                'num_foods' : row[7],
                'num_clothes' : row[8],
                'num_celebs' : row[9],
                'num_occupations' : row[10],
                'text' : row[11]
            })

    self.getInput(
        self.templates[ random.randint(0, len(self.templates) - 1) ]
    )

First we open templates.csv and loop over each line. If we're on the first line, just do nothing and go to the next iteration of the loop. We use csv.reader to parse the current line of the CSV into a list named row. Then we append a dictionary containing the data of our current line to our self.templates list variable. After reading all of our data in the CSV, randomly select one of our list elements and send it to the self.getInput method.

def getInput(self, templateDict):
    if int(templateDict.get('num_adjs')) > 0:
        self.setClassListByName('adjective', input(f"Enter {templateDict.get('num_adjs')} adjectives separated by commas: ").split(','))
    if int(templateDict.get('num_nouns')) > 0:
        self.setClassListByName('noun', input(f"Enter {templateDict.get('num_nouns')} nouns separated by commas: ").split(','))
    if int(templateDict.get('num_numbers')) > 0:
        self.setClassListByName('number', input(f"Enter {templateDict.get('num_numbers')} numbers separated by commas: ").split(','))
    if int(templateDict.get('num_colors')) > 0:
        self.setClassListByName('color', input(f"Enter {templateDict.get('num_colors')} colors separated by commas: ").split(','))
    if int(templateDict.get('num_places')) > 0:
        self.setClassListByName('place', input(f"Enter {templateDict.get('num_places')} places separated by commas: ").split(','))
    if int(templateDict.get('num_verbs')) > 0:
        self.setClassListByName('verb', input(f"Enter {templateDict.get('num_verbs')} verbs separated by commas: ").split(','))
    if int(templateDict.get('num_persons')) > 0:
        self.setClassListByName('person', input(f"Enter {templateDict.get('num_persons')} people separated by commas: ").split(','))
    if int(templateDict.get('num_foods')) > 0:
        self.setClassListByName('food', input(f"Enter {templateDict.get('num_foods')} foods separated by commas: ").split(','))
    if int(templateDict.get('num_clothes')) > 0:
        self.setClassListByName('clothing', input(f"Enter {templateDict.get('num_clothes')} articles of clothing separated by commas: ").split(','))
    if int(templateDict.get('num_celebs')) > 0:
        self.setClassListByName('celebrity', input(f"Enter {templateDict.get('num_celebs')} celebrities separated by commas: ").split(','))
    if int(templateDict.get('num_occupations')) > 0:
        self.setClassListByName('occupation', input(f"Enter {templateDict.get('num_occupations')} jobs separated by commas: ").split(','))

    self.setTemplate(templateDict.get('text'))

For each type of placeholder, we see if our dictionary variable has any corresponding data. For example,

if int(templateDict.get('num_adjs')) > 0:

checks if the number of adjectives in our template is greater than 0.

self.setClassListByName('adjective', input(f"Enter {templateDict.get('num_adjs')} adjectives separated by commas: ").split(','))

has a lot going on so let's break it down.

input(f"Enter {templateDict.get('num_adjs')} adjectives separated by commas: ")

This would display Enter 5 adjectives separated by commas: (assuming that our template has 5 adjectives) and input reads the user's entered data from the screen.

.split(','))

takes the data read in from input and splits values by comma and turns them into a list.

self.setClassListByName('adjective', VARIABLE)

calls the self.setClassListByName method with the string adjective and the list that we built from split.

self.setTemplate(templateDict.get('text'))

From our template dictionary variable, get the text of the Madlib which also contains the placeholders. Finally, send that data to self.setTemplate method.

So to sum up this whole method... for each type of placeholder we ask the user for input if the Madlib template has that type of placeholder. Then we take that user-provided data (formatted as a list) along with the placeholder type and send it to the self.setClassListByName method. Finally, we get the Madlib text containing the placeholders that we'll fill with user data and send it to self.setTemplate method.

def setClassListByName(self, listName, data):
    try:
        if listName not in self.listNames:
            raise Exception(f'{listName} not in list of names')
        setattr(self, listName, data)
    except:
        print('Error!')

If listName (adjective in our example) is in our self.listNames list, use setattr to save our data to the corresponding list. Else we're trying to use an invalid placeholder type so we raise an exception and print "Error!".

def fillTemplate(self):
    for listName in self.listNames:
        if self.getClassListByName(listName) is None:
            continue
        if listName == 'noun':
            self.fillNouns(self.getClassListByName('noun'))
            continue

        for ln in self.getClassListByName(listName):
            self.replaceCurrentSlug(ln, self.setSlug(listName))

We loop over each element in self.listNames and check to see if self.getClassListByName(listName) returns None. If it is None, it means we have no data for that type of placeholder so we skip to the next iteration of the loop. If the list is nouns, get the noun data from self.getClassListByName and pass it along to the self.fillNouns method. Otherwise... get the data for the current listName and loop over each data element. For each data element, call self.replaceCurrentSlug with the current listName and the current slug from self.setSlug.

When this method is complete, our Madlib content will have placeholders replaced with the user's provided words.

def fillNouns(self, nouns):
    for noun in nouns:
        next_noun_pos = self.template.find(self.setSlug('noun'))
        next_pluralnoun_pos = self.template.find(self.setSlug('plural_noun'))

        if next_noun_pos == -1 and next_pluralnoun_pos == -1:
            continue

        if next_noun_pos == -1:
            self.replaceCurrentSlug(self.inflector.plural(noun), self.setSlug('plural_noun'))
        else:
            self.replaceCurrentSlug(noun, self.setSlug('noun'))

Since nouns can be singular or plural, we're handling them separately from all of the other placeholder types. We loop over all of our nouns.

next_noun_pos = self.template.find(self.setSlug('noun'))
next_pluralnoun_pos = self.template.find(self.setSlug('plural_noun'))

We use Python's find function to get the next position of our noun placeholder and then do the same thing for the next plural noun placeholder.

if next_noun_pos == -1 and next_pluralnoun_pos == -1:
    continue

If the position of the next noun and plural noun is -1, skip this time through the loop. This shouldn't ever happen.

if next_noun_pos == -1:
    self.replaceCurrentSlug(self.inflector.plural(noun), self.setSlug('plural_noun'))

If there are no more singular nouns (-1 for position means not found), use inflect to get the plural form of the current noun and send it to self.replaceCurrentSlug.

else:
    self.replaceCurrentSlug(noun, self.setSlug('noun'))

The next noun to replace is singular so send it to self.replaceCurrentSlug.

def getTemplate(self):
        return self.template

Alright! Finally a simple method. We just return our self.template variable which contains the final Madlib content with the user-provided words replacing the placeholders.

Sample Output

Leave Me A Message... I Get Lonely

If you have feedback or make your own Madlib templates, please share them in the comments below.

Support This Site By Buying Me A Coffee!

If you find this tutorial helpful, please consider buying me a coffee. Thanks!