Skip to content

perf: optimize lower with list comprehension - #15229

Closed
Miladkhoshdel wants to merge 2 commits into
TheAlgorithms:masterfrom
Miladkhoshdel:perf/optimize-lower-comprehension
Closed

perf: optimize lower with list comprehension#15229
Miladkhoshdel wants to merge 2 commits into
TheAlgorithms:masterfrom
Miladkhoshdel:perf/optimize-lower-comprehension

Conversation

@Miladkhoshdel

Copy link
Copy Markdown
Contributor

Describe your change:

Optimize the existing ASCII lower() implementation by replacing the explicit append loop with a list comprehension.

The updated implementation preserves the existing behavior while improving runtime performance.

Benchmark results using timeit.repeat show a median improvement of roughly 9–13% across the tested input sizes.

Benchmark code

from statistics import median
from timeit import repeat

ASCII_UPPERCASE_START = ord("A")
ASCII_UPPERCASE_END = ord("Z")
ASCII_CASE_OFFSET = ord("a") - ord("A")

def new_lower(word: str) -> str:
"""
Convert ASCII uppercase letters in a string to lowercase.

>>> new_lower("wow")
'wow'
>>> new_lower("HellZo")
'hellzo'
>>> new_lower("WHAT")
'what'
>>> new_lower("wh[]32")
'wh[]32'
>>> new_lower("whAT")
'what'
"""
start = ASCII_UPPERCASE_START
end = ASCII_UPPERCASE_END
offset = ASCII_CASE_OFFSET

return "".join(
    [
        chr(code + offset) if start <= (code := ord(char)) <= end else char
        for char in word
    ]
)

def old_lower(word: str) -> str:
"""
Convert ASCII uppercase letters in a string to lowercase.

>>> old_lower("wow")
'wow'
>>> old_lower("HellZo")
'hellzo'
>>> old_lower("WHAT")
'what'
>>> old_lower("wh[]32")
'wh[]32'
>>> old_lower("whAT")
'what'
"""
result = []

for char in word:
    code = ord(char)
    if ASCII_UPPERCASE_START <= code <= ASCII_UPPERCASE_END:
        char = chr(code + ASCII_CASE_OFFSET)
    result.append(char)

return "".join(result)

def benchmark(
text: str,
number: int = 1_000,
repeats: int = 10,
) -> None:
"""Benchmark the old and new implementations."""
assert old_lower(text) == new_lower(text)

old_times = repeat(
    lambda: old_lower(text),
    number=number,
    repeat=repeats,
)
new_times = repeat(
    lambda: new_lower(text),
    number=number,
    repeat=repeats,
)

old_min = min(old_times)
old_median = median(old_times)
old_max = max(old_times)

new_min = min(new_times)
new_median = median(new_times)
new_max = max(new_times)

speedup = old_median / new_median
improvement = (1 - new_median / old_median) * 100

print(f"Input length: {len(text):,}")
print(f"Iterations:   {number:,}")
print(f"Repeats:      {repeats}")
print()
print(f"Old: min={old_min:.6f}s, median={old_median:.6f}s, " f"max={old_max:.6f}s")
print(f"New: min={new_min:.6f}s, median={new_median:.6f}s, " f"max={new_max:.6f}s")
print()
print(f"Median speedup:     {speedup:.2f}x")
print(f"Median improvement: {improvement:.2f}%")
print("-" * 70)

if name == "main":
sample = "Hello WORLD 123! "

for multiplier in (10, 100, 1_000, 10_000):
    benchmark(sample * multiplier)
Benchmark results

python3 benchmark_lower.py
Input length: 170
Iterations: 1,000
Repeats: 10

Old: min=0.008750s, median=0.008759s, max=0.008812s
New: min=0.007604s, median=0.007921s, max=0.010934s

Median speedup: 1.11x
Median improvement: 9.57%

Input length: 1,700
Iterations: 1,000
Repeats: 10

Old: min=0.082310s, median=0.082881s, max=0.084548s
New: min=0.071824s, median=0.072757s, max=0.073110s

Median speedup: 1.14x
Median improvement: 12.22%

Input length: 17,000
Iterations: 1,000
Repeats: 10

Old: min=0.807068s, median=0.810171s, max=0.828573s
New: min=0.704969s, median=0.710480s, max=0.771348s

Median speedup: 1.14x
Median improvement: 12.30%

Input length: 170,000
Iterations: 1,000
Repeats: 10

Old: min=7.946771s, median=7.990341s, max=8.148172s
New: min=6.880901s, median=6.913833s, max=6.931525s

Median speedup: 1.16x
Median improvement: 13.47%

  • Add an algorithm?
  • Fix a bug or typo in an existing algorithm?
  • Add or change doctests? -- Note: Please avoid changing both code and tests in a single pull request.
  • Documentation change?

Checklist:

  • I have read CONTRIBUTING.md.
  • This pull request is all my own work -- I have not plagiarized.
  • I know that pull requests will not be merged if they fail the automated tests.
  • This PR only changes one algorithm file. To ease review, please open separate PRs for separate algorithms.
  • All new Python files are placed inside an existing directory.
  • All filenames are in all lowercase characters with no spaces or dashes.
  • All functions and variable names follow Python naming conventions.
  • All function parameters and return values are annotated with Python type hints.
  • All functions have doctests that pass the automated testing.
  • All new algorithms include at least one URL that points to Wikipedia or another similar explanation.
  • If this pull request resolves one or more open issues then the description above includes the issue number(s) with a closing keyword: "Fixes #ISSUE-NUMBER".

@algorithms-keeper algorithms-keeper Bot added awaiting reviews This PR is ready to be reviewed enhancement This PR modified some existing files labels Sep 8, 2026
@Miladkhoshdel
Miladkhoshdel force-pushed the perf/optimize-lower-comprehension branch 3 times, most recently from 99d8001 to 7f410e9 Compare September 8, 2026 16:49
@Miladkhoshdel
Miladkhoshdel force-pushed the perf/optimize-lower-comprehension branch 2 times, most recently from 776d31b to 3dba0bc Compare September 8, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting reviews This PR is ready to be reviewed enhancement This PR modified some existing files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant