Skip to content

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.

"straße".lower() == "STRASSE".lower()         # False
"straße".casefold() == "STRASSE".casefold()   # True

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:

Unicode 16.0.0 and 17.0.0, core specification, chapter 3 — headings read from the published chapter, 2026-09-08
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:

Read from docs.python.org, 2026-09-08 — a dated table, not example output
  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 ToLowercaseiterators, 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

  1. Take the 297 code points where casefold() and lower() 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 a lower()-based comparison for three different reasons.
  2. Write same_word(a, b) and test it on straße/STRASSE, ΟΔΟΣ against the same word spelled with a medial sigma, İstanbul/istanbul and Content-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.
  3. str.title() and string.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.
  4. 'İ'.casefold() is two code points, one of them a combining mark. Work out what happens if you build a dictionary key with s.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