I had good votes when answering this same question's duplicate.
My answer was:
Jinja2.
Nice syntax, good customization possibilities.
Integrates well. Can be sandboxed, so you don't have to trust completely your template authors. (Mako can't).
It is also pretty fast, with the bonus of compiling your template to bytecode and cache it, as in the demonstration below:
>>> import jinja2
>>> print jinja2.Environment().compile('{% for row in data %}{{ row.name | upper }}{% endfor %}', raw=True)
from __future__ import division
from jinja2.runtime import LoopContext, Context, TemplateReference, Macro, Markup, TemplateRuntimeError, missing, concat, escape, markup_join, unicode_join
name = None
def root(context, environment=environment):
l_data = context.resolve('data')
t_1 = environment.filters['upper']
if 0: yield None
for l_row in l_data:
if 0: yield None
yield unicode(t_1(environment.getattr(l_row, 'name')))
blocks = {}
debug_info = '1=9'
This code has been generated on the fly by Jinja2. Of course the compiler optmizes it further (e.g. removing if 0: yield None
)