Veit's Blog

News from The Ministry of Bad Ideas, or: Delayed F-Strings

Aug 2 2025

I’ve been writing a lot of Python code that does string templating lately. Historically, I’ve been using format or Mustache for that, but in the last few years, f-strings have increasingly become my simple templating engine of choice, especially for shorter templates or those that need embedded logic. I still have to use jinja2 a whole lot in other contexts, and string.Template also exists, but neither sparks joy in me, so I reach for f-strings whenever I can.

Now, the issue with f-strings is that they are immediate. They evaluate in place. Why is this sometimes a problem? Occasionally, I want to define a template now and fill it in later:

my_template = f"""{name.capitalize()}:

{description}
"""

# somewhere else entirely in my code
my_template.format(
	name="entity",
	description="description of my entity"
)

I could _almost_ do this with trusty old str.format, but then I lose the ability to embed logic directly in the template. Of course, this is rarely an issue, and has a simple solution: put it in a function.

def my_template(name, description):
	return f"""{name.capitalize()}:

{description}
"""

# somewhere else entirely in my code
my_template("entity", "description of my entity")

But where’s the fun in that? Let’s do something unnecessary and complex instead. It’ll blow up in production and give you the perfect excuse to earn some on-call bucks.

Building an object

So what we are going to do is create an object we can call format on as expected. We want something that behaves like f-strings and str.format simultaneously, evaluated on-demand with variable bindings, but still supporting logic. Let’s call it Fstr.

class Fstr:
	def __init__(self, string):
		self.string = string
		
	def format(self, **kwargs):
		# TODO: what do we do here?
		pass
		
my_template = Fstr("""{name.capitalize()}:

{description}
""")

# somewhere else entirely in my code
my_template.format(
	name="entity",
	description="description of my entity"
)

So far, so unimpressive. But what do we do now? As always, the answer is to use eval! It’s dangerous, slow, and therefore perfect.

class Fstr:
	def __init__(self, string):
		self.string = string
		
	def format(self, **kwargs):
		expr = f"f'''{self.string}'''"
		return eval(expr, {}, kwargs)
		
my_template = Fstr("""{name.capitalize()}:

{description}
""")

# somewhere else entirely in my code
my_template.format(
	name="entity",
	description="description of my entity"
)

So what’s actually happening here? What does f"f'''{self.string}'''" mean? Naturally, we are turning a regular string into an f-string literal using an f-string!

In case that explanation didn’t help, let’s visualize what this expands to.

# the input
string = """{name.capitalize()}:

{description}
"""

# the f-string
f"f'''{string}'''"

# the output
f'''{name.capitalize()}:

{description}
'''

So now we have an appropriate string literal. All we need to do to close the loop is eval it and give that evaluation the appropriate bindings. Luckily, the bindings already got passed into the function in the appropriate format (keyword argument handling did it for us), and we can just pass them into eval as is!

That’s all, folks!

I hope you enjoyed this slight return into weird meta-programming territory. A bit more tame than what long-time readers might be used to, but it excited me enough to write it up anyway.

See you around!