Accueil / Blog / Métier / 2016 / The world's simplest Python template engine

The world's simplest Python template engine

Par Eric Brehault publié 19/02/2016
Let's have fun with Python format()
The world's simplest Python template engine

A template engine is a component able to render some data using a given string template.

We use it extensively in web development (that's not surprising because web development is mainly about reading some data and render them as strings, that's what I say when someone ask me about my job).

So there are already a lot of Python based template engines (Jinja2, Mako, Chameleon, ...).

The purpose of this article is to build a very basic template engine based only on the Python standard string class formatting capacities.

You probably know...

You probably know .format() replaces the old "%" based formatting solution:

>>> "My name is %s and I am %d" % ("Eric", 41)
'My name is Eric and I am 41'
>>> "My name is {name} and I am {age}".format(name="Eric", age=41)
'My name is Eric and I am 41'

It allows to perform all the typical formatting features, for instance:

>>> '{:^20}'.format('centered')
' centered '
>>> '{:^20}'.format('align right')
' align right '
>>> '{:>20}'.format('align right')
' align right'
>>> '{:<20}'.format('align left')
'align left '

and many other things (have a look to https://pyformat.info/).

Did you know?

format() is also able to access attributes or items of the parameters.

For instance with a dictionary, we can do this:

>>> 'Name: {person[name]}, age: {person[age]}'.format(person={'name': 'Eric', 'age': 41})
'Name: Eric, age: 41'

And the same goes with attributes:

>>> class Song(object):
... title = 'Where is my mind'
...
>>> 'My favorite song is: {song.title}'.format(song=Song())
'My favorite song is: Where is my mind'

That's really cool. It starts looking like a template engine, right?

Ok, but few things are missing

What we usually expect from a template engine is to be able to:

  • call methods,
  • make loops over iterables,
  • manage condition.

Let's see how we can handle that.

Calling methods

A method is an attribute of an object, we can call attribute, why couldn't wecall a method? Let's try:

>>> 'My name is {name.upper}'.format(name='eric')
'My name is <built-in method upper of str object at 0x7f67dc8d1630>'

Yeah, not exactly what we expected...

An interesting feature of format() is the format specification: instead of just inserting a field with {field}, we can specify a format like this: {field:spec}.

That's exactly what we do with float for instance:

>>> "{:.3}".format(3.14159)
'3.14'

Well, it is actually very easy to implement our own spec by derivating the Formatter class. So let's implement a ':call' spec in charge of calling the current field:

class SuperFormatter(string.Formatter):

    def format_field(self, value, spec):
        if spec == 'call':
            return value()
        else:
            return super(SuperFormatter, self).format_field(value, spec)

We can use it that way:

>>> sf.format('My name is {name.upper:call}', name="eric")
'My name is ERIC'

Nice!

Loops

Similarly, we can implement a :repeat spec,

class SuperFormatter(string.Formatter):

    def format_field(self, value, spec):
        if spec.startswith('repeat'):
            template = spec.partition(':')[-1]
            if type(value) is dict:
                value = value.items()
            return ''.join([template.format(item=item) for item in value])
        else:
            return super(SuperFormatter, self).format_field(value, spec)

Here, we pass a parameter to the spec to provide the template to use when we iterate on the loop items. So the resulting format is: <field>:repeat:<template>.

This subtemplate is a regular format() template where we escape the curly brackets by doubling them, and where the only field is the loop variable (named item).

So we can use it like this:

>>> sf.format('''Table of contents:
... {chapters:repeat:Chapter {{item}}
... }''', chapters=["I", "II", "III", "IV"])
'''Table of contents:
Chapter I
Chapter II
Chapter III
Chapter IV
'''

Condition

Let's also implement a :if spec to test the field value, and then display or not the subtemplate:

class SuperFormatter(string.Formatter):

    def format_field(self, value, spec):
        if spec.startswith('if'):
            return (value and spec.partition(':')[-1]) or ''
        else:
            return super(SuperFormatter, self).format_field(value, spec)

At first, it seems stupid, because it looks like it will only be able to conditionnally display static portions, like this:

>>> sf.format('Action: Back / Logout {manager:if:/ Delete}', manager=False)
'Action: Back / Logout '
>>> sf.format('Action: Back / Logout {manager:if:/ Delete}', manager=True)
'Action: Back / Logout / Delete'

What if we want to render conditionnally a portion of template containing fields, like this:

>>> sf.format('Action: Back / Logout {manager:if:/ Delete {id}}', manager=False, id=34)
'Action: Back / Logout '
>>> sf.format('Action: Back / Logout {manager:if:/ Delete {id}}', manager=True, id=34)
'Action: Back / Logout / Delete 34'

Hey that works! That was unexpected. To be honest, I first wrote the test exactly like this, and I expected it to fail, but it was not! Why is that?

Because here the curly brackets are not escaped, so they are processed by the main format() call. So cool!

Here we go

That's it, we have a pretty nice and complete template engine.

The full code of our world's simplest Python template engine is here, the implementation itself is 10 lines long, Python is a really powerful language!

If you have any funny idea to improve it (remember, I want to keep it short), pull requests are welcome.

The objective is mostly to have fun with Python and demonstrate what the standard Formatter class is able to do, but, having a second though, I might intregate it in Rapido :).

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
Pourquoi mettre à jour son Python (régulièrement) Pourquoi mettre à jour son Python (régulièrement) 03/12/2018

Le début des années 2010 a vu des centaines d’articles parler du passage de Python 2 à Python ...

Formation initiation Python à Nantes du 10 au 12 décembre Formation initiation Python à Nantes du 10 au 12 décembre 13/11/2018

Vous êtes développeur et maîtrisez déjà un langage de programmation ? Python vous tente et ...

Formation Python scientifique / Data scientist du 26 au 30 novembre à Toulouse Formation Python scientifique / Data scientist du 26 au 30 novembre à Toulouse 16/10/2018

Participez à notre prochaine session de formation Python scientifique à Toulouse !

10 choses qui me font aimer Python 10 choses qui me font aimer Python 08/10/2018

La 7e va vous surprendre ! Haha, non en fait il fallait que je m'arrête à 10, sinon vous ne ...

Machine Learning : classer automatiquement vos données à l'import Machine Learning : classer automatiquement vos données à l'import 20/03/2018

Comment utiliser des algorithmes de machine learning pour importer correctement des données dans ...