Lowercasing is not folding¶
Level: 201 · for Python programmers
One line: 'ß'.lower() is 'ß' and 'ß'.casefold() is 'ss', so a.lower() == b.lower() reports two spellings of one German word as two different words — and that is not a bug in lower(), because case conversion and case folding are two different operations and only one of them is asking your question.
The first line is the fix everyone reaches for, and it is wrong. It is wrong quietly, on a small fraction of inputs, in a way that survives every test written in English — which is the profile of a bug that ships. The reason is not that lower() is broken but that it was built to answer a different question: lower() produces the lowercase form of the text, something a person can read, while a caseless comparison wants a key, something no one reads and two equal words agree on. Unicode defines both, separately, and Python gives you both, separately. ß is where they visibly part company: it is already lowercase, so lower() has nothing to do to it, while its folded key is ss.
Everything else on this page follows from taking that seriously. If case conversion is a text-to-text transformation rather than a character-to-character one, then it may change a string's length, it may depend on where in the word a letter sits, and it may depend on what language the text is in. All three happen. Python handles the first two and deliberately declines the third.
Verified output of lowercasing_is_not_folding_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE CLAIM, IN ONE PAIR OF WORDS
Two spellings of one German word. A reader calls them the same word.
'straße' len 6
'STRASSE' len 7
lower() 'straße' vs 'strasse' equal = False
upper() 'STRASSE' vs 'STRASSE' equal = True
casefold() 'strasse' vs 'strasse' equal = True
lower() is the folklore fix for case-insensitive comparison and it
is the one that fails. casefold() is the operation Unicode defines
for the question 'is this the same word ignoring case'.
2. THE CHARACTERS THAT DO IT
code point char lower() upper() casefold() why
----------------------------------------------------------------------------------
U+00DF 'ß' 'ß' 'SS' 'ss' no capital of its own to come back from
U+1E9E 'ẞ' 'ß' 'ẞ' 'ss' the capital added in 2008; lower stops at ß
U+017F 'ſ' 'ſ' 'S' 's' long s -- already lowercase, so lower() is a no-op
U+00B5 'µ' 'µ' 'Μ' 'μ' MICRO SIGN; folding sends it to Greek small mu
U+03C2 'ς' 'ς' 'Σ' 'σ' final sigma; folding erases the positional form
Checked over all 1,114,112 code points: 297 of them have a
casefold() that is not their lower(). Every one is a character where
'lowercase both sides and compare' gives the wrong answer.
3. A STRING CAN GET LONGER WHEN YOU CHANGE ITS CASE
method code points mapping to more than one character longest
----------------------------------------------------------------------
upper() 102 3
lower() 1 2
casefold() 104 3
title() 48 3
Nothing ever gets shorter: methods with a code point mapping to
zero characters = 0.
One character in, three characters out:
'ΐ' (U+0390) .upper() -> 'Ϊ́' (U+0399 U+0308 U+0301)
'ΰ' (U+03B0) .upper() -> 'Ϋ́' (U+03A5 U+0308 U+0301)
'ὒ' (U+1F52) .upper() -> 'Υ̓̀' (U+03A5 U+0313 U+0300)
And exactly one code point in the whole table grows under lower():
'İ' (U+0130) .lower() -> 'i̇' (U+0069 U+0307)
-- the Turkish dotted capital I, which section 5 comes back to.
4. AND CASE IS NOT A PER-CHARACTER OPERATION AT ALL
Greek sigma has two lowercase forms. Which one you get depends on
where the letter sits in the word, so the whole string is the input.
string .lower() per character same? code points of .lower()
------------------------------------------------------------------------------------
ΟΔΟΣ οδος οδοσ False U+03BF U+03B4 U+03BF U+03C2
ΟΔΟΣ. οδος. οδοσ. False U+03BF U+03B4 U+03BF U+03C2 U+002E
ΣΟΦΟΣ σοφος σοφοσ False U+03C3 U+03BF U+03C6 U+03BF U+03C2
ΑΣΑ ασα ασα True U+03B1 U+03C3 U+03B1
Σ σ σ True U+03C3
Over every two-character string from the Greek block, 127 of them
lower differently as a string than character by character. So
''.join(c.lower() for c in s) is not a refactoring of s.lower().
casefold() takes the other road and erases the distinction:
'ΟΔΟΣ'.lower() 'οδος' U+03BF U+03B4 U+03BF U+03C2
'ΟΔΟΣ'.casefold() 'οδοσ' U+03BF U+03B4 U+03BF U+03C3
-- which is the point of folding: two spellings, one key.
5. THE CONTEXT PYTHON DOES NOT TAKE: THE LANGUAGE
In Turkish, i and ı are two different letters, and so are İ and I.
Casing them correctly needs to know the text is Turkish. Python's
case methods take no such argument -- there is nowhere to say it:
'I'.lower('tr') -> TypeError
str.upper('i', 'tr') -> TypeError
So the same four letters give the same answers on every machine,
in every locale, and none of them is the Turkish answer:
char Python .lower() Python .upper() Turkish wants agrees?
------------------------------------------------------------------------------------
U+0049 U+0069 U+0049 U+0131 / U+0049 False
U+0069 U+0069 U+0049 U+0069 / U+0130 False
U+0130 U+0069 U+0307 U+0130 U+0069 / U+0130 False
U+0131 U+0131 U+0049 U+0131 / U+0049 True
The 'Turkish wants' column is written out by hand from the
conditional section of the Unicode data. Python implements the
UNconditional mappings and not the language-tailored ones, which
is a documented choice, not an omission -- and it is why 'I'.lower()
is 'i' here whatever LANG says.
Note the shape of the failure. Uppercasing merges two Turkish
letters: 'ı'.upper() is 'I' and 'i'.upper() is 'I'.
Lowercasing splits one into two code points:
'İ'.lower() 'i̇' U+0069 U+0307 len 2
'İ'.casefold() 'i̇' U+0069 U+0307 len 2
'İstanbul'.casefold() == 'istanbul'.casefold() -> False
-- so casefold() does not rescue Turkish either. It is the right
answer to a question that is not about a specific language.
6. THERE IS A THIRD CASE, AND IT IS A CHARACTER
code point category the char .upper() .lower() .title()
------------------------------------------------------------------
U+01C4 Lu 'DŽ' 'DŽ' 'dž' 'Dž'
U+01C5 Lt 'Dž' 'DŽ' 'dž' 'Dž'
U+01C6 Ll 'dž' 'DŽ' 'dž' 'Dž'
U+01C5 is titlecase: not the uppercase form, not the lowercase
form, a third one. That is why case has three methods and not two.
7. THREE ANSWERS TO 'TITLE CASE', DISAGREEING ON ONE LINE
input .title() string.capwords() .capitalize()
------------------------------------------------------------------------------------
"o'brien and sons" "O'Brien And Sons" "O'brien And Sons" "O'brien and sons"
"dženan o'brien" "Dženan O'Brien" "Dženan O'brien" "Dženan o'brien"
"DON'T PANIC" "Don'T Panic" "Don't Panic" "Don't panic"
'hello world' 'Hello World' 'Hello World' 'Hello world'
Read the first two columns against each other and neither wins.
.title() breaks after every uncased character, so the apostrophe
starts a new word: it gets O'Brien right and Don'T wrong, from the
same rule. capwords() splits on whitespace and capitalizes only the
first letter of each piece, so it gets Don't right and O'brien
wrong -- also from one rule. And capwords() rejoins with a single
space, so it edits text you did not ask it to edit.
All three do handle the third case correctly: the leading dž becomes
the titlecase Dž (U+01C5) and not the capital DŽ (U+01C4).
They agree about Unicode and disagree about what a word is.
8. NONE OF THE CONVERSIONS IS REVERSIBLE
char .upper() then .lower() .lower() back where it started?
--------------------------------------------------------------------
'ß' 'SS' 'ss' 'ß' False
'fi' 'FI' 'fi' 'fi' False
'ʼn' 'ʼN' 'ʼn' 'ʼn' False
'ı' 'I' 'i' 'ı' False
'ς' 'Σ' 'σ' 'ς' False
'µ' 'Μ' 'μ' 'µ' False
'ſ' 'S' 's' 'ſ' False
Applying one twice changes nothing the second time -- methods that
are not idempotent over the whole table: 0.
But swapcase() is not its own inverse: 104 code points do not
come back. 'ß' -> 'SS' -> 'ss', and the ß is gone for good.
9. WHAT TO USE
the question you are asking the method
------------------------------------------------------------------------
is this the same word, ignoring case casefold() on both sides
show this to a person, uppercased upper()
show this to a person, lowercased lower()
a heading, English, no apostrophes title() -- and read it after
a person's name none of them; store what they typed
an ASCII protocol token lower() is safe, the input is ASCII
The last two rows are the ones that save time. Case conversion is
for display; case folding is for comparison; and a name is neither,
because no rule in the table knows about McDonald or van der Berg.
lower() and casefold() disagree on 297 code points, and each one is a place the folklore fix is wrong. They are not exotic: ß is ordinary German, ς is every Greek word that ends in sigma, µ is the micro sign that arrives from any system that typed µs rather than μs, and ſ is what OCR of a pre-1800 book hands you. The fix is one word — use casefold() on both sides — and it is worth writing into whatever you use for lookup keys before the data arrives rather than after.
A string can get longer when you change its case, and that is why case cannot be a per-character operation. Over the whole code space, 102 code points uppercase to more than one character, and sixteen of those reach three: fourteen Greek vowels carrying two diacritics, plus the two Latin ligatures — 'ffi'.upper() is 'FFI' and 'ffl'.upper() is 'FFL'. ß → SS is the famous one and the shortest. This kills the obvious optimisation — an in-place uppercase into a fixed buffer — in any language where you can attempt it. Python cannot attempt it, because str is immutable and every case method allocates, so the cost of the rule here is invisible and the correctness is free.
Exactly one code point gets longer under lower(), and it is the one this page comes back to twice. İ (U+0130, the Turkish dotted capital I) lowercases to i plus a combining dot above: two code points where there was one. It is worth pausing on, because it means len(s.lower()) == len(s) is not an invariant either, and because 'İstanbul'.casefold() == 'istanbul'.casefold() is False — a login form that folds both sides still rejects the city.
The other half of "not per-character" is context. Greek sigma has two lowercase forms, σ and ς, and which one is correct depends on whether the letter ends a word. str.lower() implements that rule: 'ΟΔΟΣ'.lower() is 'οδος' with a final sigma, while lowercasing the same string one character at a time gives 'οδοσ' with a medial one. Over every two-character string drawn from the Greek block, 127 differ between the two approaches. So ''.join(c.lower() for c in s) is not a refactoring of s.lower() — it is a different function that agrees on English. casefold() goes the other way and maps both sigmas to σ, which is exactly right for a key: two spellings of one word must not produce two keys.
Turkish is the case Python does not handle, and it does not handle it on purpose. In Turkish, i and ı are separate letters with separate capitals, so 'I'.lower() should be 'ı' and 'i'.upper() should be 'İ'. Python gives 'i' and 'I'. That is not a locale that failed to load: there is nowhere to pass the language at all — 'I'.lower('tr') is a TypeError, because the method takes no arguments. Unicode splits its case mappings into unconditional ones, which every implementation applies, and conditional ones, which are tailored by language; Python implements the first set and not the second. The practical shape of the failure is worth memorising, because it is asymmetric: uppercasing merges ı and i into one I, and lowercasing İ splits it into two code points. Neither direction round-trips, and folding does not rescue either.
This is also why the example prints no locale and sets none. The Turkish answer depends on a language tag that Python's str methods cannot receive, so there is no environment in which this program's output changes — which is the honest way to show a locale-dependent topic in a library whose examples must print the same thing on Ubuntu and macOS. The table's "Turkish wants" column is written out by hand from the conditional mappings, and labelled as such, rather than obtained from a locale that may or may not be installed. If you need real Turkish casing, that is a job for ICU (PyICU), and the reason is structural rather than a missing feature.
There is a third case, and it is a character. Dž (U+01C5) is General_Category Lt: not the uppercase DŽ, not the lowercase dž, a titlecase form for the digraph when it starts a capitalised word. This is the conversion half of what Is it a letter? §5 shows on the classification side, where 'Dž'.isupper() and 'Dž'.islower() are both False and istitle() is True. The third case is a real target, not a label: 'dž'.title() is 'Dž' and not 'DŽ', and all three of Python's word-capitalising functions get that right.
They get the character right and disagree about what a word is. str.title(), string.capwords() and str.capitalize() give three different results for one line of ordinary text, and — this is the part worth checking before you believe the folklore — str.title() gets O'Brien right. It titlecases any character that does not follow a cased one and lowercases the rest, so the apostrophe starts a new word: that produces O'Brien correctly and Don'T wrongly, from one rule applied consistently. string.capwords() splits on whitespace and capitalises only each piece's first letter, which produces Don't correctly and O'brien wrongly, also from one rule — and it rejoins with a single space, so it silently rewrites the runs of whitespace in text you only asked it to capitalise. Neither is a better version of the other; they fail on complementary inputs. For a person's name the answer is none of them: store what the person typed.
Nothing here is reversible, and one method is not even its own inverse. 'ß'.upper().lower() is 'ss', 'ı'.upper().lower() is 'i', 'ς'.upper().lower() is 'σ' — uppercasing then lowercasing is a normalising, lossy pass, not a round trip. Applying any one of lower, upper or casefold twice changes nothing the second time, which is what makes them usable as key functions. swapcase() is the exception: 104 code points do not come back, because ß swaps to SS and SS swaps to ss.
The Unicode Standard says section 3.13 is called something else¶
The docs for str.lower() cite the standard, and the citation is worth reading closely, because it makes the same conflation this page is about. Under Unicode 16.0.0 — the version Python 3.14 is built against, and 17.0.0 is identical in this respect — chapter 3 of the core specification lays out:
3.13 Default Case Algorithms
3.13.1 Definitions
3.13.2 Default Case Conversion <- what lower() and upper() implement
3.13.3 Default Case Folding <- what casefold() implements
3.13.4 Default Case Detection
3.13.5 Default Caseless Matching
So 3.13 is Default Case Algorithms, and "Default Case Folding" is one of its five subsections. Reading the published Python docs against that gives a clean before-and-after:
version str.lower() str.upper() str.casefold()
------------------------------------------------------------------------------------------------------
3.11 "section 3.13" "section 3.13" "section 3.13"
3.12 3.13 'Default Case Folding' 3.13 'Default Case Folding' 3.13 'Default Case Folding'
3.13 3.13 'Default Case Folding' 3.13 'Default Case Folding' 3.13 'Default Case Folding'
3.14 3.13 'Default Case Folding' 3.13 'Default Case Folding' 3.13 'Default Case Folding'
3.15 / dev 3.13.2 'Default Case 3.13.2 'Default Case 3.13.3 'Default Case
Conversion' Conversion' Folding'
3.11 was right by saying less. It cited the section number and no title, and the section number is correct for all three methods — case conversion and case folding really do both live under 3.13.
3.12 through 3.14 are wrong twice over. They attach the title "Default Case Folding" to section 3.13, which is not what 3.13 is called, and by doing so they name the wrong sub-algorithm for two of the three methods: lower() and upper() implement 3.13.2, Default Case Conversion. The upper() line is the one that gives it away — an uppercasing algorithm described as case folding is not a loose citation, it is the wrong operation. The link target agrees: 3.14's citation points at anchor #G33992 in the Unicode 16.0.0 chapter, and that anchor is "3.13 Default Case Algorithms". The docs link to the right place and quote it by a name it does not have.
It is already fixed, and the fix has not been backported. On main — the 3.15 line — each method now cites its own subsection, folding to 3.13.3 and the two conversions to 3.13.2. As of 2026-09-08 the three released branches still carry the old text, so a reader on 3.12, 3.13 or 3.14 is told that str.upper() is documented in a section about folding. Nothing has been reported upstream from here — the finding is written down and the decision about whether to ask for a backport is Adam's, not this page's.
The Rust view¶
Rust makes the length change visible in the type. char::to_uppercase and char::to_lowercase return ToUppercase and ToLowercase — iterators, not char — because one character in does not mean one character out, and the signature says so at the call site rather than in a note. That is the whole contrast in one line: Python hides the same fact inside a method that happens to return a str of any length, and Rust puts it in the type where you cannot skip it. The sibling library owns this: Meet the char ↗ is where the iterator return type is met.
The second contrast is a language refusing to guess. str::eq_ignore_ascii_case folds exactly twenty-six letters and says so in its name, so it is correct for HTTP header names and hex digits and obviously wrong for anything a person typed — where Python's lower() looks Unicode-aware and is still not a caseless comparison. And Rust std has no case folding at all: Comparing strings ↗ works through the same ß and Turkish İ cases and ends at a crate, which is the one row where str.casefold() being in the standard library is a straightforward Python win. The split between char::to_lowercase and str::to_lowercase is the same split as §4 above, made explicit: only the str method knows about final sigma, because only it can see the neighbouring characters.
If you are coming from ABAP¶
TRANSLATE lv_text TO UPPER CASE is the counterpart, and the first thing to carry across is that it is a statement that modifies its operand in place, where Python's s.upper() returns a new string and leaves s alone — so s.upper() on a line by itself does nothing at all, and that is a real beginner bug worth naming. The deeper difference is historical: ABAP predates Unicode, and a non-Unicode system uppercased against a code page, which is why the old advice was that TRANSLATE ... TO UPPER CASE on a field containing anything outside the system's code page was not to be trusted. A Unicode-enabled system casing German text will uppercase ß — check on your own system what it produces and whether the field is long enough, because a target field sized to the source is exactly the trap the ß → SS expansion sets, and truncation there is silent. There is no ABAP counterpart to casefold(): case-insensitive comparison is conventionally done by uppercasing both sides, which is the trick that works for ß and fails for Turkish and for final sigma. Never quote an SAP code-page number without verifying it against the system in front of you. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Take the 297 code points where
casefold()andlower()disagree and group them by what folding does: which are expansions likeß→ss, which are one-to-one moves likeµ→μ, and which are the Greek positional forms? The three groups fail alower()-based comparison for three different reasons. - Write
same_word(a, b)and test it onstraße/STRASSE,ΟΔΟΣagainst the same word spelled with a medial sigma,İstanbul/istanbulandContent-Type/content-type. Get three of the four right, then say in one sentence why the fourth cannot be done without knowing the text is Turkish. str.title()andstring.capwords()fail on complementary inputs. Find a third input — not an apostrophe and not a run of spaces — where they disagree, and decide which one you would rather have to explain to the person whose name it is.'İ'.casefold()is two code points, one of them a combining mark. Work out what happens if you build a dictionary key withs.casefold()and look it up with a string that arrived NFC-normalised. Then check whether normalising before or after folding fixes it — the order is not free.
See also¶
- Is it a letter? — the classification half of this page:
islower,isupper,istitle, and what a cased character is - Normalization — the other transformation you run before comparing, and the one whose order against folding matters
- Sorting is not comparing — the same gap between a code-point operation and a human rule, in ordering
stripis a set, not a prefix — anotherstrmethod that does what it says and not what its name suggests- The crosswalk — which idea lives in which library
- Preparing a string ↗ — folding as one step of a normalisation pipeline, with the step order pinned
- The table has a version ↗ — why every count on this page carries a Unicode version; these particular ones are stable from 14.0.0 to 16.0.0
str.casefold()in the Python docs ↗ — the definition, and the citation this page checks