Python - Capitalize First Letter of Each Word in a String (including after punctuation)

27 Apr 2017

Python’s str.title() and string.capwords(str) methods have some flaws. Namely:

str = "my dog's bone/toy"
assert str.title() == "My Dog'S Bone/Toy"
assert string.capwords(str) == "My Dog's Bone/toy"

As you can see, str.title() doesn’t quite format the string “Dog’s” correctly - it instead opts to convert the “‘s” to uppercase. This results in somewhat nonsensical strings.

string.capwords(str) is better about honoring the possessive case, but it has its own flaws. Firstly - it doesn’t recognize words that occur after common punctuation like /, (), -, _, etc. This means that “bone/toy” will only be converted to “Bone/toy”

I’ve written my own answer to this problem, hopefully it helps anyone else who needs to capitalize the first letter of each word after punctuation without using title() or simply relying on capwords().

My solution first takes advantage of title() to solve most of the capitalization. It then uses a Regular Expression to look for an upper-cased letter preceded by a single quote mark that is in turn preceded by a lower-cased letter(this solves the issue of retaining contractions while matching single quotes within strings.)

import re
import string

def lowercase_match_group(matchobj):
    return matchobj.group().lower()

# Make titles human friendly
# http://daviseford.com/python-string-to-title-including-punctuation
def title_extended(title):
    if title is not None:
        # Take advantage of title(), we'll fix the apostrophe issue afterwards
        title = title.title()

        # Special handling for contractions
        poss_regex = r"(?<=[a-z])[\']([A-Z])"
        title = re.sub(poss_regex, lowercase_match_group, title)

    return title

def title_one_liner(title):
    return re.sub(r"(?<=[a-z])[\']([A-Z])", lambda x: x.group().lower(), title.title())

str = "my dog's bone/toy has 'fleas' -yikes!"
assert title_extended(str) == "My Dog's Bone/Toy Has 'Fleas' -Yikes!"

# Note the errors that would occur with native implementations
assert str.title() == "My Dog'S Bone/Toy Has 'Fleas' -Yikes!"
assert string.capwords(str) == "My Dog's Bone/toy Has 'fleas' -yikes!"