The tiniest of Python templating engines

In someone else’s project (which they’ll doubtless tell you about themselves when it’s done) I needed a tiny Python templating engine. That is: I wanted to be able to say, here is a template string, please substitute a bunch of variables into it. Now, Python already does this, in about thirty different ways, and str.format or string.Template do most of it as built-in.

str.format works like this:

"My name is {name} and I am {age} years old".format(name="Stuart", age=43)

and string.Template like this:

t=string.Template(
    "My name is $name and I am $age years old"
    ).safe_substitute(name="Stuart", age=43)

Both of which are pretty OK.

However, what they’re missing is loops; having more than one of a thing in your template, and looping over a list, substituting it each time. Every even fractionally-more-featureful templating system has this, whether Mustache or Jinja or whatever, of course, but I didn’t want another dependency. All I needed was str.format but with loops. So, I thought, I’ll write one, in about four lines of code, so I can just drop the function in to my Python file and then I’m good.

def LoopTemplate(s, ctx):
    def loophandler(m):
        md = m.groupdict()
        return "".join([LoopTemplate(md["content"], val)
                        for val in ctx[md["var"]]])
    return re.sub(r"\{loop (?P<var>[^}]+)\}(?P<content>.*?)\{endloop\}",
                  loophandler, s, flags=re.DOTALL).format(**ctx)

And lo, twas so. So I can now do

LoopTemplate(
    "I am {name} and my imps' names are: {loop imps}{name}{endloop}",
    {
        "name": "Stuart",
        "imps": [
            {"name": "Pyweazle"}, {"name": "Grimthacket"}, {"name": "Hardebon"}
        ]
    }
)

and it all works. Not revolutionary, of course, but I was mildly pleased with myself.

Much internal debate about whether loophandler() should have been a lambda, but I eventually decided it was more confusing that way, on the grounds that it was confusing me and I knew what it was meant to be doing.

A brief explanation: re.sub lets you pass a function as the thing to replace with, rather than just a string. So we find all examples of {loop something}...{endloop} in the passed string, look up something in the “context”, or the dict of substitution variables you passed to LoopTemplate, and then we call LoopTemplate again, once per item in something (which is expected to be a list), and pass it the ... as its string and the next item in something as its context. So it all works. Of course, there’s no error handling or anything — if something isn’t present in the context, or if it’s not a list, or if you stray in any other way from the path of righteousness, it’ll incomprehensibly blow up. So don’t do that.

More in the discussion (powered by webmentions)

  • Peter Langridge responded at twitter.com The tiniest of Python templating engines. kryogenix.org/days/2020/01/1…
  • Peter Langridge responded at twitter.com
  • Alan Pope 🍺 🐧 🐱 🇬🇧 🇪🇺 responded at twitter.com
  • Abhinay Khoparzi responded at twitter.com
  • lamby responded at twitter.com Is that Victor Mono...? 🎩👌
  • Stuart Langridge responded at twitter.com the font? Not sure; I used @carbon_app to make a nice screenshot of the code, 'cos it's pretty (although I had to turn off the window decorations beca…
  • Bruno Bord responded at twitter.com The tiniest of Python templating engines. kryogenix.org/days/2020/01/1…
  • Prashant Gupta responded at twitter.com
  • Matt Walker 🚵 responded at twitter.com
  • MgcLabs responded at twitter.com
  • Simon Davy responded at twitter.com