The whole exercise is deciding which of these two questions to ask, and in which order. Read what 111 does to each.
print(111 % 10) # -> 1 the last digit
print(111 % 100) # -> 11 the last two digits
# The last digit says 'st'. The last two digits say 'this is a teen'.
# 111th is correct, so the second question has to be asked first.
print({1: 'st', 2: 'nd', 3: 'rd'}.get(7, 'th')) # -> 'th'
print({1: 'st', 2: 'nd', 3: 'rd'}.get(2, 'th')) # -> 'nd'
- % 10 and % 100 answer different questions
The remainder after dividing by ten is the last digit; after dividing by a hundred it is the last two. The teens exception is about the last two digits, and that is why checking only the last digit produces 11st.
- The exception is checked before the rule it breaks
If the general rule runs first, 11 has already been answered 'st' by the time anyone asks whether it is a teen. Special cases go on top. This is the same ordering as a guard clause, and the same reason.
- A dict with .get() replaces three branches
The mapping from last digit to suffix is data — three pairs and a default. dict.get(key, fallback) is the lookup and the else in one call, which is the same idea the if-chain refactor exercise is built on.
There is a general rule based on the last digit, and an exception based on the last two. Work out which one has to be tested first by trying 11 against both orders on paper.
number % 100 in (11, 12, 13) identifies the exception, and it works for 111 and 211 too. If that is true, the suffix is th and you are done.
Guard the negative. Check the teens exception and return early. Otherwise look up number % 10 in a small dict of {1: 'st', 2: 'nd', 3: 'rd'} with 'th' as the default, and build the answer with an f-string.
def ordinal(number): if number < 0: raise ValueError(...) if number % ____ in (11, 12, 13): return f'{number}th' suffix = {1: 'st', 2: 'nd', 3: 'rd'}.____(number % ____, '____') return f'{number}{suffix}'
SUFFIXES = {1: "st", 2: "nd", 3: "rd"}
def ordinal(number):
if number < 0:
raise ValueError(f"number cannot be negative, got {number}")
if number % 100 in (11, 12, 13):
return f"{number}th"
return f"{number}{SUFFIXES.get(number % 10, 'th')}"
Swap the two checks and the function is wrong for eleven numbers in every hundred — and right for the first ten, which is exactly the range anybody types while testing. When a rule has exceptions, the exceptions go first, and a test for one of them belongs in the case set on day one.
Writing `if number in (11, 12, 13)` passes every case up to a hundred and then quietly breaks at 111. Using the remainder states the actual rule — 'the last two digits are a teen' — instead of enumerating the instances you happened to think of.
SUFFIXES.get(number % 10, 'th') is a lookup with a fallback, which is what the rule is. Lifting it to module level means the dict is built once rather than on every call, and it names the thing so a reader does not have to infer that a mapping is what they are looking at.
This is English, and ordinals are one of the least portable things in an interface — many languages inflect them by gender and case, and some do not use a suffix at all. If the text will be translated, an ordinal belongs in a locale-aware formatter such as ICU, not in your own function. And for dates specifically, check whether you need an ordinal at all: most style guides and every ISO format write 2026-09-12 rather than 'the 12th'.
- Return just the suffix, so a caller can style the 'st' smaller than the number the way print typography does.
- Write the words instead — 'first', 'second', 'twenty-first' — and notice where the modulo approach stops helping.
- Find out what your locale library does for ordinals in two other languages, and decide whether this function should exist in a product that ships outside English.