September 09, 2011

Python Templating with @stringfunction

I've posted a new version of the templet.stringfunction utility I have discussed before. I have been using it to make a bunch of web UI in programming projects with my son (actually pretty sophisticated little projects - This version reflects what's needed for a 2011-era web app.

What's New

This version is still svelte, clocking in at 91 lines of python code (plus comments and tests). Version 3.3 changes the following from v2:

  • The old class-template idioms in the original version have been removed; they were slow and unwieldy. Now it's just @stringfunction and @unicodefunction.
  • Common uses of $ in javascript no longer need to be escaped in templates. Jquery $. and $( are very common, and regular expressions often have $/ and $' and $", so all these sequences now pass through the template without escape.
  • Line numbers are aligned exactly so that both syntax errors and runtime errors in exception traces are reported on the correct line of the template file in which the code appears.

Together these small changes - particularly accurate error line numbers - make @stringfunction much more usable for composing large templates for website development.


Usage is unchanged: just annotate a python function with @stringfunction or @unicodefunction, and then put the template text where the docstring would normally be. Leave the function body empty, and efficient code to concatenate the contents will be created.

  from templet import stringfunction
  def myTemplate(animal, body):
    "the $animal jumped over the $body."
  print myTemplate('cow', 'moon')

This is turned into something like this:

  def myTemplate(animal, body):
    out = []
    out.append("the ")
    out.append(" jumped over the ")
    return ''.join(out)

There are just six constructs that are supported, all starting with $:

  • $myvar inserts the value of the variable 'myvar'
  • ${...} evaluates the expression and inserts the result
  • ${[...]} runs a list comprehension and concatenates the results
  • ${{...}} executes enclosed code; use 'out.append(text)' to insert text
  • $$ an escape for a single $
  • $ (at the end of the line) is a line continuation

All ordinary uses of $ in the template need to be escaped by doubling the $$ - with the exception of (as mentioned above) $., $(, $/, $', and $".


The philosophy behind templet is to introduce only the concepts necessary to simplify the construction of long strings in python; and then to encourage all other logic to be expressed using ordinary python.

A @stringfunction function can do everything that you can do with any function that returns a string: it can be called recursively; it can have variable or keyword arguments; it can be a member of a package or a method of a class; and it can access global imports or invoke other packages. As a result, although the construct is extremely simple, it brings all the power of python to templates, and the @stringfunction idea scales very well.

Beyond simple interpolation, templet does not invent any new syntax for data formatting. If you want to format a floating-point number, you can write ${"%2.3f" % num}; if you want to escape HTML sequences, just write ${cgi.escape(message)}. Not as brief as a specialized syntax, but easy to remember, brief enough, and readable to any python programmer.

Similarly, templet does not invent any new control flow or looping structures. To loop a template, you need to use a python loop or list comprension and call the subtemplate as a function:

 def doc_template(table):
   <h1>${ }</h1>
     for item in table:

If you prefer list comprehensions, it is slightly more brief:

 def doc_template(table):
   <h1>${ }</h1>
   ${[self.row_template(item) for item in table]}

The design encourages simple templates that read in straight-line fashion, an excellent practice in the long run. Although when invoking subtemplates you need to pass state, of course you can use @stringfunction to make methods and pass state on "self", or use object parameters.

Details and Style

Some tips/guidelines for using these annotations.

Whitespace can be important inside HTML, but for python readability you often want to indent things, so @unicodefunction / @stringfunction gives you a few tools:

  1. It identifies the number of leading spaces that are uniformly used to the left of the template and strips them.
  2. It strips the first line of the template, if empty.
  3. It allows you to use a $ at the end of a line for a line continuation.

So my recommended style for multiline templates is:

  • indent template text in the function as if it were python code.
  • use a python triple-quote and put the opening quote on its own line.
  • never indent HTML tags - they just get too deep, so put them all at column 0.
  • when nesting gets confusing, for readability, just put one tag on each line.
  • liberally use $ continuations if layout demands no-whitespace.
  • indent code inside ${{ and then put }} on its own line (a newline right after a closing }} is eaten).

Relative indenting for python code inside ${{...}} is preserved using the same leading-space-stripping trick as is used for the templates themselves, so you can indent embedded python as normal, and you can start the indenting at whichever column feels natural. I usually indent embedded python by one more level.

In the unusual case where it is necessary to emit text that has leading spaces on every line, you can begin the template with a continuation line with the $ in the column that you want to treat as column zero.

One question is whether the opening """ should be on the same line as the def or its own line. Either style is supported - for line number purposes, the program source is just scanned to discover the position of the opening quote - but for clarity I usually put the opening quote on its own line.

For example, if you want to achieve all on one line the following:

<tr><td class="..."><a class="..." href="/foo/bar/...">....</a></td><td class="...">...</td></tr>

Then you could use:

def table_row(row_data):
  <td class="${col1_class} def">$
  <a class="${link_class}"$
   href="/foo/bar/${cgi.escape(filename, True)}">$
  <td class="${col2_class}">$
    if (enrolled): out.append('enrolled')
Posted by David at September 9, 2011 01:08 PM


Excellent, elegant work! I love finding gems like this.

One thing though. First time users should beware testing templet in interactive mode. The following line (148) fails silently:

docline, (source, _) = 2, inspect.getsourcelines(func)

The reason is that a func, if defined in interactive mode, does not have source code to scan.

I added two lines to re-raise the IOError with a message warning against the use of functions defined interactively. I doubt it's worth the extra 2 lines when a warning in the documentation should suffice.


Posted by: Rob at February 20, 2012 11:12 AM

Hi Rob, thanks for the bug report.

I have posted a version 3.3 which fixes this bug so that templet can be used in interactive mode (it only needs the sourcelines to align line numbers for error messages; if it can't find them, then it can just guess when there is an error).

Posted by: David at February 21, 2012 08:02 AM

Would be perfect if indentation was kept, e.g. in the following example:

def templettemplate(lines):
____def foo():
print templettemplate('a = 5\nb = 7')

it prints:
def foo():
____a = 5
b = 7

instead of:
def toto():
____a = 5
____b = 7

PS: sorry for the underscores - indents are also eaten by the comment :-)

Posted by: maxime-esa at April 19, 2013 11:33 AM

Beautiful. Prototyping my concept is a real joy using this template tool!

Posted by: thouters at October 29, 2013 04:35 PM

thanks for sharing your nice ideas.

this template script unfortunately seems to not work with encoding/decoding utf-8, could you please have a look to correct it?

def unicode_test(text):


outputs the following:

Traceback (most recent call last):
File "./", line 76, in
File "./", line 72, in unicode_test
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe0' in position 4: ordinal not in range(128)

Posted by: Berteh at March 23, 2017 07:14 PM

Posted by: Barbara at November 29, 2018 06:10 AM

Posted by: Nicholas at December 10, 2018 02:18 AM jordan supreme shirt salomon speedcross 3 Pandora Bracelet mcm handbags salomon shoes pandora earrings air max 97 silver bullet michael kors purse vapormax burberry scarf adidas yeezy ed hardy clothing rihanna puma shoes polo ralph lauren christian louboutin moncler coat mont blanc outlet pandora ralph lauren dresses pandora jewelry jordan air max 97 mens nike air max 95 kyire 3 kd shoes salvatore ferragamo shoes fitflop sandals christian louboutin pandora jewelry official site fitflops uk barbour jacket true religion sale goyard bag adidas originals nmd yeezy jimmy choo shoes air max 90 michael kors valentino sandals nike air max 97 stephen curry ultra boost jordan shoes mbt mens shoes air jordan 11 space jam moncler jacket mens yeezy boost 350 v2 pandora salvatore ferragamo kate spade bags stephen curry shoes lebron goyard bag adidas superstar burberry shirt nike huarache jordan 11 birkenstock birkenstock sandals nike kd nike vapor max nike vapormax under armour yeezy boost huaraches Yeezy Boost 350 nike air max 97 ray ban salomon boots nike vapormax chrome hearts bracelet ultra boost celine wallet red bottoms pandora bracelet moncler jacket womens adidas ultra boost fitflops clearance nike air max 97 skechers boots ralph lauren shirts ray ban prescription sunglasses bulgari jewelry prada handbags mulberry handbags cartier pandora charms air max 90 coach bags prada purse mcm handbags kd shoes lacoste sale nike air max 97 ralph lauren hermes kyrie asics running shoes air max 90 burberry scarf asics running shoes balenciaga shoes jordan 11 space jam nmd adidas moncler coat nike air max 95 true religion jeans men air max nike air vapormax moncler balenciaga kate spade handbags nmd chrome hearts coach bags kate spade bags nike air max 2018 christian louboutin moncler jacket mens pandora jewelry official site north face jacket Salomon Shoes oakley prescription sunglasses lebron james shoes north face jacket nike vapor max nike air max 270 men ecco sandals supreme shoes harden vol 1 pandora Hermes Birkin Bags chrome hearts clothing longchamp handbag fitflop shoes longchamp le pliage

Posted by: chrome hearts ring at January 15, 2019 12:33 AM yeezy shoes mbt fitflop pandora yeezy boost nike air max moncler jacket mens bvlgari ring ultra boost nike air max 2018 air max 90 balenciaga bag huaraches adidas superstar cartier glasses nike air max 270 nmd salomon speedcross 4 true religion mont blanc pens Yeezy Shoes coach outlet air jordan jordan 11 north face coats burberry handbags Pandora Charms oakley sunglasses birkenstock sale polo mulberry sale north face outlet pandora fitflop kevin durant shoes huarache curry shoes adidas yeezy longchamp handbag lacoste sale pandora bracelet nike air max goyard wallet longchamp le pliage red bottoms moncler jacket pandora bracelet nike air max 95 yeezy boost 350 v2 supreme shirt salomon air max ferragamo shoes men air jordan chrome hearts pendant kyrie irving yeezy boost 350 didas ultra boost lebron james shoes chrome hearts jimmy choo ed hardy ray ban prada wallet michael kors fitflops clearance burberry bags kd shoes off white nike nike air max 90 ecco boots mcm bags barbour jacket pandora bracelet celine purse kate spade pandora jewelry ralph lauren polo shirts nike air max 90 louboutin moncler coat balenciaga bags louboutin kyire 3 prada bags Hermes Belts nike air max coach handbags kd 9 skechers boots kate spade jordan pandora jewelry official site moncler jacket mens hermes belt women lebron 14 james harden shoes goyard true religion jeans men nike air vapormax asics salomon nike air max 90 ultra boost mens air max 97 womens air max 95 mens supreme clothing ralph lauren curry 4 red valentino ralph lauren dresses mcm bags pandora necklace michael kors bags nike air max birkenstock fitflop sale air max 95 mens kate spade moncler jacket air max 97 nike air vapormax flyknit air jordan burberry wallet christian louboutin nike air max 270 women chrome hearts ring puma rihanna pandora rings ray ban glasses Salomon nmd moncler jacket ferragamo jordan retro 12 under armour outlet asics shoes

Posted by: kate spade at January 15, 2019 08:51 PM
Post a comment

Remember personal info?