This solved function collapses runs of whitespace. It is the same two-step shape your answer needs — replace the runs, then tidy the ends.
import re
def collapse_spaces(text):
return re.sub(r"\s+", " ", text).strip()
# collapse_spaces(' too many spaces ') -> 'too many spaces'
- The + is what collapses runs
\s matches one whitespace character; \s+ matches as many in a row as it can and treats them as a single match. That is why three spaces become one and not three. Without the +, every replacement is one-for-one and nothing collapses.
- re.sub returns a new string
Strings are immutable, so nothing is edited in place — the result is handed back and the original is untouched. This is also why the two steps chain onto one line so naturally.
- Tidy the ends afterwards, not inside the pattern
You could write a cleverer regular expression that never produces edge whitespace. It would be longer, harder to read and easier to get wrong. Doing the simple substitution and then stripping is the version you will still understand in a year.
Do the lowercasing first — it means your pattern only has to think about a-z, never A-Z. Then there are exactly two jobs left: replace everything that is not allowed, and clean up the ends.
Inside a character class, a leading ^ means 'anything except these'. So [^a-z0-9] matches one disallowed character, and adding + makes a whole run of them match at once — which is what turns ', ' into a single hyphen.
re.sub(pattern, '-', title.lower()) gives you a slug that may have hyphens at one or both ends. str.strip accepts the characters to remove, so .strip('-') finishes the job. The whole function is one line.
import re def slugify(title): return re.sub(r"[^____]____", "-", title.____()).strip("____")
import re
def slugify(title):
return re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
Doing .lower() first halves the pattern: [^a-z0-9] instead of [^a-zA-Z0-9]. Every character you do not have to describe is a character the pattern cannot get wrong. Normalising input before you inspect it is a habit that pays far beyond regular expressions.
'---abc---'.strip('-') removes hyphens from both ends until it meets something else — it is not removing the string '-' once. That also means strip('-') on '-' gives '', which is exactly what the 'nothing usable' case needs. Worth knowing it treats the argument as a set of characters, because people expect the other behaviour and are surprised by 'xhellox'.strip('xoh').
slugify(slugify(x)) == slugify(x) here, which is why re-saving a post cannot change its URL. Anything that ends up in a link, a filename or a key should be checked for this — the 'already hyphenated' case exists to pin the behaviour down so a later refactor cannot quietly break it.
This drops every non-ASCII character, so 'Café' becomes 'caf' and a title written entirely in Hindi, Arabic or Chinese becomes an empty slug. For an English-language blog that is acceptable; for anything international it is not. Real slug libraries transliterate first — unicodedata.normalize('NFKD', title) will split 'é' into 'e' plus a combining accent, which the pattern then discards, giving 'cafe'. Beyond Latin scripts you need a proper transliteration library or a different design, such as a numeric id with the title as decoration.
- Add NFKD normalisation so 'Café Con Leche' becomes 'cafe-con-leche' rather than 'caf-con-leche', and check what it does to 'Straße'.
- Add a max_length that truncates on a hyphen boundary rather than mid-word, so a long title does not end in a half-word.
- Guarantee uniqueness: given a set of slugs already in use, append -2, -3 and so on. Decide what happens when a title ends in a number already.