Skip to content

[mypyc] Add support for str.lower() and str.upper() - #20948

Merged
JukkaL merged 4 commits into
python:masterfrom
VaggelisD:str_lower_upper
Mar 4, 2026
Merged

[mypyc] Add support for str.lower() and str.upper()#20948
JukkaL merged 4 commits into
python:masterfrom
VaggelisD:str_lower_upper

Conversation

@VaggelisD

Copy link
Copy Markdown
Contributor

Fixesmypyc/mypyc#1088

Follow up on #19375 with full Unicode support:

  • ASCII fast path
  • Shared CPyStr_ChangeCase helper, parameterized by function pointers
  • _PyUnicode_ToLowerFull/_PyUnicode_ToUpperFull for Unicode which handle 1-to-N expansion (e.g., ßSS); This was a sticky point with the previous PR which relied on Py_UNICODE_TOLOWER/TOUPPER.
  • Temporary len * 3 UCS-4 buffer for the Unicode path; This is because each Unicode char may be expanded from 1 byte to 3

Benchmarks (x10 M calls each)

InputMethodmypycCPythonSpeedup
"" (empty)lower0.070s0.182s2.60x
"" (empty)upper0.069s0.181s2.62x
ASCII len 1 (convert)lower0.187s0.234s1.25x
ASCII len 1 (convert)upper0.161s0.246s1.53x
ASCII len 1 (no-op)lower0.157s0.234s1.49x
ASCII len 1 (no-op)upper0.151s0.245s1.62x
ASCII len 10lower0.189s0.267s1.41x
ASCII len 10upper0.189s0.278s1.47x
ASCII len 100lower0.434s0.510s1.18x
ASCII len 100upper0.431s0.527s1.22x
Unicode len 1 (Latin)lower0.152s0.309s2.03x
Unicode len 1 (Latin)upper0.156s0.320s2.05x
Unicode len 10 (Latin)lower0.438s0.543s1.24x
Unicode len 10 (Latin)upper0.441s0.530s1.20x
Unicode len 100 (Latin)lower2.560s2.897s1.13x
Unicode len 100 (Latin)upper2.564s2.001s0.78x
Unicode len 1 (Greek)lower0.216s0.304s1.41x
Unicode len 1 (Greek)upper0.218s0.313s1.44x
Unicode len 10 (Greek)lower0.526s0.587s1.12x
Unicode len 10 (Greek)upper0.479s0.478s1.00x
1-to-N expansion len 1lower0.332s0.318s0.96x
1-to-N expansion len 1upper0.258s0.323s1.25x
1-to-N expansion len 10lower0.614s0.655s1.07x
1-to-N expansion len 10upper0.526s0.542s1.03x

@VaggelisD

Copy link
Copy Markdown
ContributorAuthor

Oops, looks like the APIs I based the PR on are not exported past 3.13+, will need to figure out a solution. The str.lower() and str.upper() primitives continue to claim victims even after a year


// The _PyUnicode_CheckConsistency definition has been moved to the internal API
// https://github.com/python/cpython/pull/106398
#if defined(Py_DEBUG) && defined(CPY_3_13_FEATURES)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a mistake afaict, CPY_3_13_FEATURES is defined by us so that'd always evaluate to true

@VaggelisD

Copy link
Copy Markdown
ContributorAuthor

I was testing this branch on Python 3.11; For 3.13+ I had to fallback to CPython's generic dispatch for Unicode strings.

This introduces the following regression for the latter versions (tested on 3.14)

InputMethodmypycCPythonSpeedup
"" (empty)lower0.118s0.144s1.22x
"" (empty)upper0.104s0.143s1.38x
ASCII len 1 (convert)lower0.200s0.234s1.17x
ASCII len 1 (convert)upper0.196s0.232s1.18x
ASCII len 1 (no-op)lower0.194s0.224s1.15x
ASCII len 1 (no-op)upper0.199s0.229s1.15x
ASCII len 10lower0.222s0.256s1.15x
ASCII len 10upper0.226s0.256s1.13x
ASCII len 100lower0.476s0.499s1.05x
ASCII len 100upper0.490s0.510s1.04x
Unicode len 1 (Latin)lower0.404s0.305s0.75x
Unicode len 1 (Latin)upper0.420s0.313s0.74x
Unicode len 10 (Latin)lower0.652s0.544s0.83x
Unicode len 10 (Latin)upper0.566s0.481s0.85x
Unicode len 1 (Greek)lower0.406s0.308s0.76x
Unicode len 1 (Greek)upper0.404s0.303s0.75x
1-to-N len 1lower0.425s0.326s0.77x
1-to-N len 1 (ß→SS)upper0.425s0.326s0.77x

@JukkaLJukkaL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Can you also compare performance against mypyc master, on Python 3.14?

Comment threadmypyc/lib-rt/str_ops.c Outdated
Comment threadmypyc/lib-rt/str_ops.c Outdated
@JukkaL

Copy link
Copy Markdown
Collaborator

Since lower and upper calls are very common, we might eventually want to include lower case / upper case tables for at least UCS-2 within librt (that are only included in the build if the primitives are actually used, i.e. as optional files). But the ascii case is very common, so this is already helpful.

@VaggelisD

VaggelisD commented Mar 3, 2026

Copy link
Copy Markdown
ContributorAuthor

I applied the inlining and reran the benchmarks:

  • For our branch vs Python 3.14, it looks like the regressions disappeared, not entirely sure why, maybe inlining did all that magic (the previous ones were on Python 3.11)

  • For our branch vs master, the results are logical i.e regression kicks in for large Unicode strings but other than that we're on par or faster.

    InputMethodmypyc (branch)mypyc (master)Pythonvs mastervs CPython
    "" (empty)lower0.063s0.159s0.211s2.52x3.35x
    "" (empty)upper0.062s0.163s0.206s2.63x3.32x
    ASCII len 1 (convert)lower0.157s0.230s0.260s1.46x1.66x
    ASCII len 1 (convert)upper0.150s0.272s0.272s1.81x1.81x
    ASCII len 1 (no-op)lower0.156s0.229s0.269s1.47x1.72x
    ASCII len 1 (no-op)upper0.150s0.232s0.272s1.55x1.81x
    ASCII len 10lower0.189s0.260s0.310s1.38x1.64x
    ASCII len 10upper0.185s0.266s0.302s1.44x1.63x
    ASCII len 100lower0.429s0.548s0.554s1.28x1.29x
    ASCII len 100upper0.436s0.517s0.557s1.19x1.28x
    ASCII len 100 (no-op)lower0.440s0.512s0.551s1.16x1.25x
    ASCII len 100 (no-op)upper0.442s0.515s0.563s1.17x1.27x
    Unicode len 1 (Latin)lower0.353s0.354s0.392s1.00x1.11x
    Unicode len 1 (Latin)upper0.356s0.420s0.380s1.18x1.07x
    Unicode len 10 (Latin)lower0.517s0.579s0.571s1.12x1.10x
    Unicode len 10 (Latin)upper0.523s0.544s0.586s1.04x1.12x
    Unicode len 100 (Latin)lower2.407s2.476s2.431s1.03x1.01x
    Unicode len 100 (Latin)upper2.587s2.645s2.637s1.02x1.02x
    Unicode len 1 (Greek)lower0.357s0.353s0.396s0.99x1.11x
    Unicode len 1 (Greek)upper0.362s0.347s0.392s0.96x1.08x
    Unicode len 10 (Greek)lower0.582s0.564s0.643s0.97x1.10x
    Unicode len 10 (Greek)upper0.577s0.572s0.672s0.99x1.16x
    1-to-N len 1 (İ)lower0.375s0.364s0.415s0.97x1.11x
    1-to-N len 10 (İ)lower0.641s0.632s0.676s0.99x1.05x
    1-to-N len 1 (ß→SS)upper0.330s0.324s0.370s0.98x1.12x
    1-to-N len 10 (ß)upper0.613s0.609s0.648s0.99x1.06x
    1-to-N len 1 (ffi→FFI)upper0.350s0.341s0.385s0.97x1.10x

@JukkaLJukkaL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks good! I'm glad that we finally have primitives for these common operations.

@JukkaL
JukkaL merged commit e6d41eb into python:masterMar 4, 2026
18 checks passed
@VaggelisD
VaggelisD deleted the str_lower_upper branch March 5, 2026 08:11
p-sawicki pushed a commit that referenced this pull request Jun 1, 2026
#21553)
6th PR of #21418. This PR introduces two `i32 -> i32` case-conversion helpers, alongside
the existing classifiers.
**The constraint to flag**: A single i32 holds one codepoint, but some
Unicode case mappings expand to multiple e.g `'ß'.upper()` becomes
`'SS'`, `'fi'.upper()` becomes `'FI'` etc.
For those inputs the primitive _returns the input unchanged_; This is
the same split CPython makes between `Py_UNICODE_TOUPPER` (codepoint)
and `str.upper()` (string), with the former returning the **first
codepoint** of the expansion.
Users needing full Unicode case conversion should call `s.upper()` /
`s.lower()` on the string, for which we already have mypyc primitives
(#20948). For ASCII benchmarks, the codepoint primitives are ~5x faster
than their `str` counterparts, avoiding the 1-char allocation.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add primitives for str lower() and upper()

2 participants

@VaggelisD@JukkaL