Two built-ins that look like the answer. Read the second line of each — that is where they part company with what you want.
print("don't stop believing".title())
# -> "Don'T Stop Believing" capitalises after the apostrophe too
print("THE GREAT GATSBY".capitalize())
# -> "The great gatsby" only the very first letter, whole string
print("the"[0].upper() + "the"[1:])
# -> "The" explicit, and does exactly one thing
- .title() capitalises after every non-letter
It has no idea what a word is — it capitalises any letter that follows something which is not a letter. Apostrophes, hyphens and digits all trigger it, so O’Brien survives by luck while don’t does not. Handy for a quick script, wrong for anything a person reads.
- .capitalize() works on the whole string, not each word
It uppercases the first character and lowercases everything else — which is exactly what you want *per word*, and not what you want for a title. Applying it word by word is close to the answer; the small-word rule is what is left.
- You need the position, not just the word
'the' is capitalised at the start of a title and lowercase in the middle, so the same word has two answers depending on where it sits. That is what enumerate is for — the moment a rule mentions 'the first', the index has to be in scope.
Split into words, decide what each one should look like, then join them back up. The only hard part is that the decision depends on where the word is, not only what it is.
Keep the small words in a set. A word stays lowercase when it is in that set *and* it is not the first word — so you need its index. enumerate gives you both at once.
Lowercase each word first, so shouting input is handled once and for all. Then for each word: if the index is greater than zero and the word is in the small set, keep it lowercase; otherwise take word[0].upper() plus the rest. Join with single spaces.
SMALL = {'a', 'an', 'the', 'of', 'in', 'on', 'at', 'and', 'or', 'to', 'for'} def title_case(text): words = [] for index, word in ____(text.split()): word = word.____() if index > ____ and word in SMALL: words.append(word) else: words.append(word[0].____() + word[1:]) return ' '.____(words)
SMALL_WORDS = {"a", "an", "the", "of", "in", "on", "at", "and", "or", "to", "for"}
def title_case(text):
words = []
for index, word in enumerate(text.split()):
word = word.lower()
if index > 0 and word in SMALL_WORDS:
words.append(word)
else:
words.append(word[0].upper() + word[1:])
return " ".join(words)
Calling .lower() on every word before anything else means the shouting case, the mixed case and the ordinary case all become the same problem. Normalising up front and deciding afterwards keeps the interesting logic free of case handling — the same shape as stripping and lowercasing before a lookup in parse_bool.
Without the index you would need a separate pass for the first word, or a flag, or slicing the list into head and tail. enumerate makes the exception a single extra condition on the line that already exists. Whenever a specification says 'except the first', reach for it.
Here they behave identically, because .lower() has already run. They diverge on input like 'iPhone': .capitalize() would give 'Iphone', while the explicit form leaves the rest of the word alone if you skip the lowering. Knowing which one you are relying on matters the day a brand name shows up.
Do not treat this as a house style. Publications disagree about which words stay lowercase, whether the *last* word is always capitalised — Chicago says yes — and what to do with hyphenated compounds and prepositions of four letters or more. If a style guide governs the output, encode that guide rather than this list. And for user-generated titles in other languages, do not title-case at all: German nouns and Turkish dotted i both make a mess of naive letter-level rules.
- Always capitalise the last word as well, which is what most style guides require, and notice that it needs the length as well as the index.
- Make the small-word list a parameter so a caller can supply their own style guide, and decide what a sensible default argument looks like given the mutable-default trap.
- Handle a title that already contains an acronym — 'the NASA years' — without lowercasing it, and work out how you would even detect that reliably.