Apparently there's a function call overhead as well that the % operator doesn't incur because
for i in xrange(10000000):
s = "%s" % i
is also faster than lstr = str
for i in xrange(10000000):
s = lstr(i)
[Edit] After thinking about it a second longer, I wonder whether there is some lookup for lstr as well even though it's local. But storing the function in lstr is faster than using str so I'm not sure how this is actually implemented. I'm sure someone here will know more.