Table of Contents

Introduction

Pretty often, I find myself in the situation that I need to generate certain reports, output files or manipulate strings. All of these situations have in common that the report generation or string manipulation follows a certain pattern. Usually, these patterns are so similar that you wish to have a template you can reuse and fill in the data you currently have. Fortunately, Python provides a class, which can help us: string.Template.

In this article, you will learn how to utilize this class to generate output files based on data you currently work with and how to manipulate strings in the same manner. Therefore, I not only use examples you may encounter in your day-to-day work but point you to an actual tool many of you may know and which uses this class for its file reports. Without further introduction, let’s jump in!

Note: This article is base on Python 3.9.0 (CPython). You can find the code examples used throughout the article on GitHub.

Motivation

Before having a look at an example, let us spend a few moments looking at the advantages of using string.Template over other solutions.

  1. No additional dependencies needed: It works out-of-the-box with the standard Python installation, so no pip install is required.
  2. It is lightweight: Of course, template engines like Jinja2 and Mako are widely used and even provide a certain amount of logic within the templates. However, in the scenarios described in this article, these capabilities are simply overkill.
  3. Separation of concerns: Instead of embedding string manipulations and report generations directly in your code, a template file can be used to move that to an external place. This allows you to exchange the template file instead of touching your code if you want to change the report’s structure or design.

Because of these advantages (among others), famous libraries and tools are making use of it. Wily [1] is only one example. At the end of 2018, the inventor and maintainer of wily, Anthony Shaw, wanted to support HTML as an output format for the reports generated by wily. I volunteered to implement it and started introducing Jinja2. However, the final output was so simple that introducing a new dependency was not justifiable. Finally, I ended up using Python’s string.Template class and you know what? It works.

Example: Generate an overview of awesome books

After discussing the motivation behind going with Python’s built-in string.Template class, we will have a look at the first real-world example. Imagine, you are working at a company publishing annual reports about the best books published in the past year. The year 2020 is special, because in addition to your annual report, you publish a list of the best books ever written.

At this point, we do not care about where the data come from or which books are part of this list. For the sake of simplicity, we assume that we have a JSON-file called data.json, which contains a mapping of author name and book title as shown below.

{
    "Dale Carnegie": "How To Win Friends And Influence People",
    "Daniel Kahneman": "Thinking, Fast and Slow",
    "Leo Tolstoy": "Anna Karenina",
    "William Shakespeare": "Hamlet",
    "Franz Kafka": "The Trial"
}

Your task is now to visualize this list of books in a way you can share with others, e.g. large magazines, companies or bloggers. The company decided that a simple table in HTML-format would be sufficient. Now the question: How to generate this HTML-table?

Of course, you can do that manually or create placeholders for each book. But looking forward, having a more generic version is highly preferred as the list can be extended and the structure or design may change.

It is the perfect time to utilize Python’s string.Template class! We start by creating the actual template, which is shown below. Here, we call the file template.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Great Books of All Time</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
</head>
<body>
    <div class="container">
        <h1>Great Books of All Time</h1>
        <table class="table">
            <thead>
                <tr>
                    <th scope="col">#</th>
                    <th scope="col">Author</th>
                    <th scope="col">Book Title</th>
                </tr>
            </thead>
            <tbody>
                ${elements}
            </tbody>
        </table>
    </div>
</body>
</html>

The file itself is very rudimentary. We use bootstrap for styling and created the basic structure of the final table. Table headers are already included, but the data is still missing. Notice, that within the tbody-element a placeholder ${elements} is used to mark the point where we will inject the list of books later on.

Perfect, we are all set up to implement the Python-script generating the desired output! Therefore, we create a new Python-file called report.py in our current working directory. First, we import the two built-in modules we will need and load the data from the JSON-file.

# report.py
import json
import string

with open("data.json") as f:
    data = json.loads(f.read())

The data variable is now a dictionary containing the author’s name (key) and the book title (value) as key-value pairs. Next, we generate the HTML-table, which we will place into our template (remember the placeholder?). Therefore, we initialize an empty string, to which we will add new table rows, as shown below.

content = ""
for i, (author, title) in enumerate(data.items()):
    content += "<tr>"
    content += f"<td>{i + 1}</td>"
    content += f"<td>{author}</td>"
    content += f"<td>{title}</td>"
    content += "</tr>"

The code snippet shows that we loop over all items in the data-dictionary and put the book title as well as the author’s name in the corresponding HTML-tags. Awesome! We created the final HTML-table. In the next step, we need to load the template file, we created earlier:

with open("template.html") as t:
    template = string.Template(t.read())

Notice, that string.Template accepts a string and not a file path. Consequently, you can also supply strings you created earlier in your program without the need of saving them to a file. In our case, we supplied the template.html file’s content to it.

Finally, we use the template’s substitute() method to replace the placeholder elements with the string stored in the variable content. The method returns a string, which we store in the variable final_output. Last but not least, we create a new file called report.html and write the final output to it.

final_output = template.substitute(elements=content)
with open("report.html", "w") as output:
    output.write(final_output)

Great, you generated your first file report! If you open the report.html file in your browser, you can see the result.

Safe substitution

Now that you built your first string.Template use case, I want to share with you a common situation plus its solution before closing this article: Safe substitution. What does that mean?

Let’s take an example: You have a string, where you want to inject a person’s first and last name. You can do it as follows:

# safe_substitution.py
import string

template_string = "Your name is ${firstname} ${lastname}"
t = string.Template(template_string)
result = t.substitute(firstname="Florian", lastname="Dahlitz")
print(result)

However, what happens if you miss to pass a value for one or the other? It raises a KeyError. To prevent this scenario, we can utilize the safe_substitution() method. In this context, safe means that Python tries to return a valid string in any case. Consequently, if no value is found, the placeholder is simply not replaced.

Let’s adjust the code as follows:

# safe_substitution.py
import string

template_string = "Your name is ${firstname} ${lastname}"
t = string.Template(template_string)
result = t.safe_substitute(firstname="Florian")
print(result)  # Your name is Florian ${lastname}

There might be use cases, where this is a more elegant solution or even a desired behaviour. However, keep in mind that this can cause unintended side effects somewhere else.

Summary

Congratulations, you made it through the article! While reading the article, you have not only learnt the basics of Python’s string.Template class as well as the motivation behind using it but implemented your first file report script, too! Furthermore, you got to know the safe_substitution() method and in which scenarios its usage may be beneficial.

I hope you enjoyed reading the article. Feel free to share it with your friends and colleagues! Do you have feedback? I am eager to hear it! You can contact me via the contact form or other resources listed in the contact section.

If you have not already, consider following me on Twitter, where I am @DahlitzF or subscribing to my newsletter! Stay curious and keep coding!

References