Python for Humans · Draft v0.1 · Oct 27, 2026 · Draft position: chapter 8 · First draft; build a follower mapping from a tiny sentence, then generate text from Shakespeare.
The Word-Prediction Generator
- You should already know
- Lists, decisions and loops; dictionaries (chapter 6); defining functions and returning values (chapter 7).
- You'll build
- A small program that generates strange new text from Shakespeare.
- You'll be able to
- Build a dictionary of follower lists, choose a random list item, and follow those choices to generate text with a stopping rule.
- You need
- This page. Python runs in the panel beside it; installing Python on your own computer comes in chapter 9.
We've been asking questions about Shakespeare's words. Now let's use them to make something.
Suppose we know which words tend to follow other words. We could pick a starting word, choose a possible follower, then repeat. Would the result sound like Shakespeare?
Sometimes. Often it will sound like a dragon trying to pass an English exam. But we'll be able to explain every choice the program makes.
What Comes Next?
Start with a tiny text we can read in one glance:
the dragon eats pizza the dragon eats soup
After 'the' comes 'dragon'. After 'dragon' comes 'eats'. After 'eats', we have two possibilities: 'pizza' and 'soup'.
We can record those possibilities in a dictionary. Type this into the Python panel, pressing Enter after each command. As before, don't type the >>> prompt.
>>> followers = {'the': ['dragon', 'dragon'], 'dragon': ['eats', 'eats'], 'eats': ['pizza', 'soup'], 'pizza': ['the']} >>> followers['eats'] ['pizza', 'soup'] >>> followers['the'] ['dragon', 'dragon']
This time the dictionary's values are lists, rather than counts. Looking up 'eats' gives us the list of words we saw immediately after it.
Why are some entries repeated? The text contains 'the dragon' twice, so we record 'dragon' twice. We'll use those repeated entries to preserve how often a pairing occurs.
There's no 'soup' key. It ends the text, so we haven't observed any word following it.
Your turn: Ask for followers['dragon']. Then check whether 'soup' is a key with 'soup' in followers. If we added 'soup tastes good' at the end of the source text, which new pairings would we need to record?
Choose a Follower
Chapter 3 used random.randint() to choose a number. The same module has random.choice(), which chooses one item from a nonempty collection such as a list.
import random choices = ['pizza', 'soup'] word = random.choice(choices) print(word)
Run it several times. You might get the same answer more than once; random choices don't have to alternate.
Now change the list to ['pizza', 'pizza', 'soup']. Each entry has the same chance of being selected, so pizza has two chances out of three. Repeated entries affect the chances, but a few runs won't necessarily show that exact proportion.
That's why we're keeping repeated followers in our dictionary. A pairing observed more often gets more entries in the list.
Try it: Replace the choices with two possible destinations in your adventure. Which line makes the choice, and which line displays it?
Remember the Previous Word
Typing the whole follower dictionary ourselves would be tedious. Let's build it with a loop.
We need to look at words in pairs. Instead of introducing another tool, we can keep track of the word we just visited. This small program shows how:
words = ['the', 'dragon', 'eats', 'pizza'] previous = '' for word in words: if previous != '': print(previous, '->', word) previous = word
On the first iteration, there's no previous word yet. We use the empty string '' to represent that, so we skip the print. Then we remember 'the' in previous.
On the second iteration, previous refers to 'the' and word refers to 'dragon'. We display the pair, then update previous to 'dragon'.
That update happens after every word, outside the if block. If we put it inside the block, the first word would never be remembered.
Your turn: Add 'the', 'dragon' at the end of the list. Predict the additional pairs before running it. The last word has no next word, so it never appears on the left unless it occurred earlier too.
Build the Dictionary
Now replace displaying each pair with recording it. Here's the function we'll use:
def next_words(text): followers = {} previous = '' for word in text.split(): if previous != '': if previous in followers: followers[previous].append(word) else: followers[previous] = [word] previous = word return followers text = 'the dragon eats pizza the dragon eats soup' followers = next_words(text) print(followers)
The outer loop and the update to previous work just as they did in Word Pairs. Inside the decision, we have two cases:
- If we've seen the previous word as a key already, append its new follower to the existing list.
- Otherwise, create that key with a list containing its first follower.
Look closely at followers[previous].append(word). First we look up the list under the previous word's key. Then we append the current word to that list. We aren't adding a new dictionary key on that line.
return followers comes after the loop, giving us the completed dictionary. It should match the dictionary we typed by hand at the beginning.
Your turn: Change the sample to 'red blue red green'. Before running it, write down what followers['red'] should contain. Then add a line to display that value and check. What should followers['blue'] contain?
We haven't used the word-cleaning rules from our counter. This generator keeps capitalization and punctuation attached to the source words. That preserves some of the source's appearance, but it means 'dragon' and 'dragon.' are different entries. It's a choice about our data, not something Python decides for us.
Take One Step
Let's try one lookup and one random choice together:
import random followers = {'eats': ['pizza', 'soup']} current = 'eats' choices = followers[current] current = random.choice(choices) print(current)
We started at 'eats', looked up its possible followers, then changed current to the selected word.
Your turn: Change the starting word and its matching dictionary key to 'visits', and supply a list of places. Predict the possible results. Can the program print a place that isn't in your list?
To generate a longer passage, we'll repeat that step. Each chosen word becomes the starting point for the next choice.
Keep Walking
Here's a small generator with the follower dictionary supplied. Run it a few times.
import random followers = { 'the': ['dragon', 'dragon'], 'dragon': ['eats', 'eats'], 'eats': ['pizza', 'soup'], 'pizza': ['the'] } current = 'the' output = current word_count = 1 while word_count < 20: if current in followers: choices = followers[current] current = random.choice(choices) output = output + ' ' + current word_count = word_count + 1 else: break print(output)
The dictionary is split over several lines here to make it easier to read. The braces still enclose one dictionary, with commas separating its entries.
output starts with the first word, so word_count starts at 1. Each iteration chooses another word, adds it to the output with a space, and increases the count.
There are two ways to finish:
- The output reaches 20 words, making the
whilecondition false. - The current word has no recorded follower, so
breakleaves the loop early.
If we choose 'soup', it's added to the output. On the next iteration, the membership check finds no 'soup' key and stops. We don't try to look it up, so there's no KeyError.
Every list in our mapping contains at least one follower, because we created it when observing a pair. That also keeps random.choice() away from an empty list.
Try it: Start at 'soup'. What happens? Then restore 'the' and change the limit to 5. You should get at most five words, including the starting word.
Generate Your Own Text
Now combine the pieces you've tried. Keep the next_words() definition from Build the Dictionary, then replace its example calls with the generation code from Keep Walking. Replace the hand-written dictionary with a call to next_words(text).
Start with the tiny text again. Check that every adjacent pair in your output exists in that source. The sentence as a whole may be new, but each step should be one we recorded.
If you need a comparison, here's a complete worked version. Try assembling yours first; you can copy the helper you've already worked through so you can concentrate on connecting the pieces.
import random def next_words(text): followers = {} previous = '' for word in text.split(): if previous != '': if previous in followers: followers[previous].append(word) else: followers[previous] = [word] previous = word return followers text = 'the dragon eats pizza the dragon eats soup' followers = next_words(text) current = 'the' output = current word_count = 1 while word_count < 50: if current in followers: current = random.choice(followers[current]) output = output + ' ' + current word_count = word_count + 1 else: break print(output)
The combined version puts the lookup directly inside random.choice(). It's the same two operations as before; keeping a separate choices variable is equally fine.
Let Shakespeare Take Over
Keep working in your generator box. Replace the sample text assignment with the file-reading command:
text = open('shakespeare.txt').read()
Leave the starting word as 'the' for your first run. Now the mapping comes from Shakespeare, but the generation loop hasn't changed.
Read the output. Is any phrase surprisingly convincing? Where does it become nonsense? Run it again and compare. The exact output will vary, and repeating a phrase is possible.
Your turn: Choose another starting word that appears in the source. Remember capitalization matters. If your word has no followers, the output will be just that word. You can check if current in followers: before generation if you want to display an explanation instead.
Change the word limit, too. A larger limit permits a longer passage; it doesn't guarantee one if the walk reaches a word with no followers.
We read the whole shared file, including its header and license. Those words can appear in the generated output too. A program follows the data we gave it, not our intention that it use only the plays.
What Did We Build?
This program makes a choice about the next word using only the current word. It doesn't remember the sentence's subject or plan an ending. Its odd changes of direction follow directly from that limitation.
The general approach is often called a Markov chain. You don't need the term to understand our code: we recorded transitions, then followed them with random choices.
Modern language models also generate text by choosing successive pieces of text, but use different machinery, much longer context, and learned patterns rather than this dictionary lookup. Our generator gives us a concrete process to inspect; it isn't an explanation of how a modern model works internally.
For now, the accomplishment is yours: you can read every line, trace a choice, and change the result.
Your Turn
Predict
Write down both outputs. Remember that a list inside a dictionary is still a list.
followers = {'the': ['dragon'], 'dragon': ['eats']} followers['the'].append('cat') print(followers['the']) print('eats' in followers)
Find the Bug
This should preserve every follower of 'eats', but keeps only the last one. Fix the loop so it adds to the list instead of replacing it each time.
followers = {'eats': []} for word in ['pizza', 'pizza', 'soup']: followers['eats'] = [word] print(followers['eats'])
Check the Helper
Go back to your generator. Before using a book, call next_words() with 'red blue red green', an empty string, and a single word. Print each returned dictionary.
What should each contain? In the last two cases there are no adjacent pairs to record. A useful helper should still finish and give back an empty dictionary.
Change the Source
Replace the sample text with a short paragraph of your own. Include some repeated words with different followers so the program has choices to make. Start with a word you know is present and compare several runs.
You can use dialogue from your adventure, for example. What kinds of repetition make the output more varied, and what kinds trap it in a cycle? Keep the word limit while you experiment.
What I've Learned
Write one sentence explaining why this program can create a new sequence while using only word pairings from its source. Add one limitation you observed in its output.
Keep What You Made
Before leaving the page, copy the contents of your finished generator into a plain-text note on your computer. Save the word counter from the earlier chapters too if you've changed it. Don't rely on an open browser tab to keep your edits.
You don't need to run those saved notes yet. In Keeping Your Work, we'll install Python, save the code as a program, and run it on your own computer.
That's the end of Part I. You've used input, decisions, repetition, collections, files and functions to build something you can understand and change. Next, we'll give that work a home beyond this page.