So you're looking at words, which at least in English are strings of characters. And you have rules that say you need to find different combinations of characters, and then do different things to them. This sounds like a job for regular expressions.
OK, this is a regular expression, but it uses a syntax you didn't see in Chapter 7, Regular Expressions. The square brackets mean “match exactly one of these characters”. So [sxz] means “s, or x, or z”, but only one of them. The $ should be familiar; it matches the end of string. So you're checking to see if noun ends with s, x, or z.
This re.sub function performs regular expression-based string substitutions. Let's look at it in more detail.
Does the string Mark contain a, b, or c? Yes, it contains a.
OK, now find a, b, or c, and replace it with o. Mark becomes Mork.
The same function turns rock into rook.
You might think this would turn caps into oaps, but it doesn't. re.sub replaces all of the matches, not just the first one. So this regular expression turns caps into oops, because both the c and the a get turned into o.
Back to the plural function. What are you doing? You're replacing the end of string with es. In other words, adding es to the string. You could accomplish the same thing with string concatenation, for example noun + 'es', but I'm using regular expressions for everything, for consistency, for reasons that will become clear later in the chapter.
Look closely, this is another new variation. The ^ as the first character inside the square brackets means something special: negation. [^abc] means “any single character except a, b, or c”. So [^aeioudgkprt] means any character except a, e, i, o, u, d, g, k, p, r, or t. Then that character needs to be followed by h, followed by end of string. You're looking for words that end in H where the H can be heard.
Same pattern here: match words that end in Y, where the character before the Y is not a, e, i, o, or u. You're looking for words that end in Y that sounds like I.
vacancy matches this regular expression, because it ends in cy, and c is not a, e, i, o, or u.
boy does not match, because it ends in oy, and you specifically said that the character before the y could not be o. day does not match, because it ends in ay.
pita does not match, because it does not end in y.
This regular expression turns vacancy into vacancies and agency into agencies, which is what you wanted. Note that it would also turn boy into boies, but that will never happen in the function because you did that re.search first to find out whether you should do this re.sub.
Regular expression substitutions are extremely powerful, and the \1 syntax makes them even more powerful. But combining the entire operation into one regular expression is also much harder to read, and it doesn't directly map to the way you first described the pluralizing rules. You originally laid out rules like “if the word ends in S, X, or Z, then add ES”. And if you look at this function, you have two lines of code that say “if the word ends in S, X, or Z, then add ES”. It doesn't get much more direct than that.