The relative speeds of str.format and %

My most recent blog post talked about the use of str.format instead of the % operator for interpolating values into strings. Some people who read the post wondered about their relative speeds.

I should first note that my first response to this is: I don’t really care that much. I’m not saying that speed isn’t important, or that optimization should never be done. Rather, my philosophy is that people are expensive and computers are cheap — and thus, anything we do to make people more productive, even if that comes at the expense of program speed, is probably fine.

Of course, that’s not always going to be true. Sometimes, you need (or just want) to squeeze more out of your computer. And to be a good programmer, you also need to know the relative advantages and disadvantages of the techniques you’re using.

So I decided to run a few, quick benchmarks on the relative speeds of str.format and %.  Sure enough, the % operator was a lot faster.  I ran my benchmarks the magic %timeit command that is built into the IPython interactive shell.  (If you’re using Python and aren’t using IPython, you should really switch ASAP.)  Note that in order to make things easier to read, I’m removing the IPython input and output prompts, and using >>> to denote where I entered text.

>>> name = 'Reuven'
>>> %timeit 'Hello there, {}'.format(name)
1000000 loops, best of 3: 243 ns per loop

>>> %timeit 'Hello there, %s' % name
10000000 loops, best of 3: 147 ns per loop

Wow.  As you can see, %timeit executed each of these lines of code 1,000,000 times. It then gave the average speed per loop. The % operator was, on average, about 100 ns faster than str.format. That shouldn’t come as a huge surprise, given that % is an operator (and thus doesn’t require a method call), doesn’t handle indexes and attributes, and can (I’m guessing) pass a great deal of its work off to C’s printf function.

Then again, is 100 ns really that long to wait for a formatted string?  I’m not so sure, to be honest.

What happens if we perform more than one interpolation?

>>> first = 'Reuven'
>>> last = 'Lerner'
>>> %timeit 'Hello there, {} {}'.format(first, last)
1000000 loops, best of 3: 371 ns per loop

>>> %timeit 'Hello there, %s %s' % (first, last)
1000000 loops, best of 3: 243 ns per loop

Each of these takes significantly longer to run than was the case with a single replacement. The difference between them continues to be about 120 ns per assignment — still not something to worry about too much, but the difference does exist.

What if I make the strings space-padded?

>>> %timeit 'Hello there, {:10} {:15}' % (first, last)
1000000 loops, best of 3: 459 ns per loop

>>> %timeit 'Hello there, %10s %15s' % (first, last)
1000000 loops, best of 3: 254 ns per loop

Now we see an even starker difference between the two ways of handling things. What about something like floating-point math, which takes longer?

>>> import math
>>> %timeit 'I love to eat {}'.format(math.pi)
1000000 loops, best of 3: 587 ns per loop

>>> %timeit 'I love to eat %f' % math.pi
1000000 loops, best of 3: 354 ns per loop

Limiting the number of decimals shown doesn’t seem to change the outcome very much:

>>> %timeit 'I love to eat {:.3}'.format(math.pi)
1000000 loops, best of 3: 582 ns per loop

>>>%timeit 'I love to eat %.3f' % math.pi
1000000 loops, best of 3: 329 ns per loop

UPDATE: Several people on Reddit took me to task for failing to consider the overhead of the str.format method call.  I mentioned this briefly above, but should have realized that there was an easy to to avoid this, namely aliasing the attributes (the method str.format and the float math.pi) to local variables:

>>> f = 'I love to eat {:.3}'.format
>>> p = math.pi
>>> %timeit f(p)
1000000 loops, best of 3: 489 ns per loop

>>> %timeit 'I love to eat %f' % p
1000000 loops, best of 3: 370 ns per loop

We still see significant overhead. Again, I’m guessing that a lot of this has to do with the overhead of a method vs. an operator. I’m not about to start looking at the bytecodes; this wasn’t meant to be a super-deep investigation or benchmark, but rather a quick check and comparison, and I think that on that front, it did the trick.

So, what have we learned?

  • Yes, str.format is slower than %.
  • The number of parameters you pass to str.format, and whether you then adjust the output with padding or a specified number of decimals, significantly influences the output speed.
  • That said, in many programs, the difference in execution speed is often 100 ns, which is not enough to cause trouble in many systems.

If speed is really important to you, then you should probably use %, and not str.format. However, if speed is more important than the maintainability or readability of your code, then I’d argue that Python is probably a poor choice of programming language.

4 thoughts on “The relative speeds of str.format and %”

  1. You said ” That shouldn’t come as a huge surprise, given that % is an operator (and thus doesn’t require a method call)”

    Most operators in Python are translated to method calls.

    ham % eggs

    is really just

    ham.__mod__(eggs)

    just like

    foo + bar

    is just

    foo.__add__(bar)

    and so on.

    1. As a general rule, you’re right — operators are turned into method calls.

      However, even without checking, I have to assume that this is not the case for strings and other compiled, built-in, fundamental types. It should surprise me greatly if every time you use % on a string, it’s really invoking a method rather than jumping directly to the right thing.

      But hey, being surprised and learning new things is part of the game here…

  2. Actually, the mod operation has it’s own bytecode instruction, and that is why it’s faster to use the operator.
    If you call the magic method explicitly, % is actually slightly slower than format.


    In [1]: timeit 'abc{}'.format('lol')
    1000000 loops, best of 3: 329 ns per loop

    In [2]: timeit 'abc%s' % ('lol',)
    10000000 loops, best of 3: 19.4 ns per loop

    In [3]: timeit 'abc%s'.__mod__(('lol',))
    1000000 loops, best of 3: 334 ns per loop

    Inspecting the generated bytecodes, It’s easy to see why:


    In [5]: dis("'abc{}'.format('lol')")
    1 0 LOAD_CONST 0 ('abc{}')
    3 LOAD_ATTR 0 (format)
    6 LOAD_CONST 1 ('lol')
    9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
    12 RETURN_VALUE

    In [6]: dis("'abc{}'.__mod__('lol')")
    1 0 LOAD_CONST 0 ('abc{}')
    3 LOAD_ATTR 0 (__mod__)
    6 LOAD_CONST 1 ('lol')
    9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
    12 RETURN_VALUE

    In [7]: dis("'abc{}' % ('lol',)")
    1 0 LOAD_CONST 0 ('abc{}')
    3 LOAD_CONST 2 (('lol',))
    6 BINARY_MODULO
    7 RETURN_VALUE

Leave a Reply

Your email address will not be published. Required fields are marked *

÷ one = seven