2025, Dec 28 11:00
Fast Palindrome Checks in Python: Slicing and Equality vs Hand-Written Loops, with Benchmarks
Case study of Python palindrome checks: slicing txt[::-1] with equality vastly outperforms hand-written loops. See timings, CPython details, and key takeaways.
Checking whether a string is a palindrome seems trivial, yet the performance difference between two common approaches can be dramatic. If you ever expected a hand-written loop to be faster because it does fewer comparisons, this case study shows why that intuition fails in Python and how to think about it correctly.
Reproducible example
The following snippet runs two implementations against the same workload and prints both the match count and the total execution time. The logic is identical, names are different for clarity.
def check_pal(txt):
for k in range(len(txt) // 2):
if txt[k] != txt[len(txt) - k - 1]:
return 0
return 1
def check_pal_sliced(txt):
if txt == txt[::-1]:
return 1
else:
return 0
RUNS = 500
REP = 99999
base = '101' * REP
import time
start = time.time()
print(sum([1 for r in range(RUNS) if check_pal_sliced(base + base[r:])]))
end = time.time()
print(f'{(end - start):.2f}')
start = time.time()
print(sum([1 for r in range(RUNS) if check_pal(base + base[r:])]))
end = time.time()
print(f'{(end - start):.2f}')
On a representative run, the outcomes were:
168
0.40
168
34.20
What really happens under the hood
The slicing-based approach runs almost entirely in optimized C code. The expression txt[::-1] is implemented in CPython with C-level optimizations, and the equality check txt == txt[::-1] is also executed in compiled code. Crucially, the comparison short-circuits and stops as soon as a mismatch is detected. No explicit Python-level loop, no per-element Python operations.
The loop-based approach is pure Python. Every iteration performs multiple interpreted bytecode operations: range iteration, indexing on the left end, indexing on the right end, and a comparison. Accessing txt[k] and txt[len(txt) - k - 1] separately incurs repeated per-element overhead. Even if you expect only len/2 comparisons, the cost per comparison is high because Python loops and indexing are orders of magnitude slower than equivalent C loops embedded in built-ins.
There is also a large, hidden cost outside both palindrome functions in this specific workload. Each check operates on base + base[r:], which constructs a fresh, very large string before the palindrome logic even starts. You pay that allocation and copy cost on every iteration, and then the slower Python loop magnifies the total time.
An incremental improvement that still loses badly
Caching the length once does reduce work inside the loop, but the overall approach remains much slower than the slicing method. This variant removes repeated len(...) calls from the hot path.
def check_pal_len_cached(txt):
n = len(txt)
for k in range(n // 2):
if txt[k] != txt[n - k - 1]:
return 0
return 1
RUNS = 500
REP = 99999
base = '101' * REP
import time
start = time.time()
print(sum([1 for r in range(RUNS) if check_pal_sliced(base + base[r:])]))
end = time.time()
print(f'{(end - start):.2f}')
start = time.time()
print(sum([1 for r in range(RUNS) if check_pal_len_cached(base + base[r:])]))
end = time.time()
print(f'{(end - start):.2f}')
A run with this tweak still shows a stark gap:
168
0.41
168
25.11
The small gain confirms that some overhead was removed, but it does not change the fundamental difference between interpreted loops and optimized C paths.
Solution: lean on slicing and equality
For palindrome checks in Python, prefer the slicing-based approach. It benefits from C-level optimizations in both reversal and comparison, and the comparison stops early on mismatches. In workloads like the one above, that effect compounds with the cost of constructing large strings before the check; minimizing Python-level work pays off quickly.
The fast path is already present in the example:
def check_pal_sliced(txt):
if txt == txt[::-1]:
return 1
else:
return 0
If a looping solution is required for readability reasons, caching the length locally is a modest improvement, but it remains much slower than the slicing approach in practice under the same workload.
Why this matters
When performance depends on operations repeated at scale, the difference between Python-level loops and vectorized or built-in operations implemented in C can be overwhelming. In this case, only a fraction of the tested inputs were palindromes, but the overall time was dominated by how the check was executed: the fast path avoided Python loops entirely, while the slow path paid per-iteration interpreter overhead and repeatedly handled large strings created just before the check.
Practical takeaways
Favor built-in operations that push work into optimized C paths. For palindrome checks, use slicing and direct equality. Be mindful of the data you feed into your checks; constructing large intermediate strings on every iteration can dwarf the actual algorithmic work. Small micro-optimizations inside Python loops, like saving len(txt) to a variable, help a bit but cannot compete with the performance of optimized built-ins combined with short-circuiting comparisons.
In short, the counterintuitive outcome here is expected for Python: fewer comparisons in Python code does not beat the same work done in compiled C behind a simple, expressive idiom.