← back to journal

A Python t-string is cheap until you render it

2026-10-14 · Kit Wickham

Python 3.14 added template strings: t"hello {name}" gives you a Template object holding the literal parts and the values, not a finished string. I wanted to know what that costs next to an f-string. Building one is cheaper than an f-string as soon as there is a format spec, because nothing gets formatted yet. Turning it into text with a plain-Python renderer costs three to four times what the f-string did.

The test

CPython 3.14.8, timeit with 200,000 calls per case, 15 rounds with the cases interleaved, median per call. Each case is a lambda, and I subtracted an empty lambda (10.8 ns). The machine had other jobs on it, so read differences under about 15% as ties.

expressionf-string (ns)t-string, built (ns)t-string, rendered (ns)
"hello {name}"2146214
"{name}: {n} items, {x:.2f}"13988511
eight {n} in a row2761891056

For scale, the three-field line was 161 ns with str.format and 127 ns with %.

Why it comes out this way

An f-string does all its work right away: it calls format() on every value and joins the pieces. A t-string only packs the values into Interpolation objects. With one plain string value, joining is nearly free, so the f-string wins (21 against 46 ns, since the template still has to allocate objects). Once there is an int or a :.2f to format, the f-string does that work and the t-string puts it off, so the t-string comes out ahead.

Putting it off doesn't make it go away. A renderer has to walk the template, apply the conversion and format spec, and join, all in Python bytecode. My straightforward one (iterate, isinstance, format, join) cost 3.7× the f-string on the three-field line. A tighter version that zips t.strings with t.interpolations and skips format when there's no spec got that to about 3.3×. Both give the same output as the f-string.

Against escaping by hand

A later check compared the fair version for HTML: <p>{name} {n} {x}</p> rendered by a small Python escaper, against an f-string that calls html.escape(str(…)) on each value. The best of four shuffled rounds came out at 583 ns for the t-string and 357 ns for the f-string, so 1.6×. Two other batches gave the same ratio within 10%. Both versions pay for the three html.escape calls, so the gap drops to the renderer's own loop, about 225 ns, which is a quarter of a millisecond for a page with a thousand values. Using zip on t.values and t.strings instead of iterating the template made no difference here (581 ns).

So what

A few hundred nanoseconds per string is nothing next to the jobs t-strings are for: escaping HTML, building SQL with parameters, structured logging. There the renderer would be doing extra work anyway, and a log line that is filtered out never has to be rendered at all, which is where the cheap build helps. Where you just need text, an f-string is still the fastest way to get it.

Method: one machine, a single process per run, about 60 lines of plain Python. These are per-call costs in a tight loop, not what a whole program would see.