Deferred Evaluation for Template Strings (original) (raw)

Interpolations of template strings are evaluated immediately upon their creation. However, in many situations, deferring the evaluation of an expression until it is actually needed can be useful. Currently, Python lacks an effective mechanism to support this capability.

Current Limitations

Within the current constraints, one approach to implementing deferred evaluation involves using lambda expressions, as demonstrated below.

template = t"Result: {(lambda: expensive_func(val1, val2, flag=True))}"

However, the problem with this approach is that it is verbose and ambiguous. Expressions such as {(lambda: ...)} are particularly cumbersome, and few developers would realistically choose to program this way, since such expressions reduce readability, clarity, and simplicity.

Furthermore, there is no clear way—either from a developer’s perspective or for the code that will later evaluate the template string—to distinguish whether a value is simply callable, or is intentionally wrapped for deferred evaluation. This ambiguity is especially problematic when the value itself has meaning as a callable object, such as a class, among other examples.

Because of these limitations, implementing deferred evaluation in current template strings is practically impossible, though not technically impossible.

!d Conversion

To address these issues, this document proposes introducing a new !d conversion (where 'd' stands for 'defer') to template strings.

This conversion wraps the internal expression in a lambda function and sets the conversion value to "d".

deferred_template = t"{1 + 2!d}"
interpolation = deferred_template.interpolations[0]

assert interpolation.conversion == "d" # Conversion is set to "d"
assert interpolation.expression == "1 + 2"
assert callable(interpolation.value) # Value is callable
assert interpolation.value() == 3 # Compute actual value when it is called

Unlike the previous approach, the conversion attribute makes it clear—both to developers and to code implementations that handle the template string—that the interpolation is deferred.

Examples

The following examples further illustrate the !d conversion.

Implementing f-string Behavior

The following code shows an example implementation of f-string-like behavior for template strings in fstring.py from the pep750-examples repository.

def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object:
    """Convert the value to a string using the specified conversion."""
    # Python has no convert() built-in function, so we have to implement it.
    if conversion == "a":
        return ascii(value)
    if conversion == "r":
        return repr(value)
    if conversion == "s":
        return str(value)
    return value

def f(template: Template) -> str:
    """Implement f-string behavior using the PEP 750 t-string behavior."""
    parts = []
    for item in template:
        match item:
            case str() as s:
                parts.append(s)
            case Interpolation(value, _, conversion, format_spec):
                value = convert(value, conversion)
                value = format(value, format_spec)
                parts.append(value)
    return "".join(parts)

Adding only two lines to this code enables support for !d conversion.

def convert(value: object, conversion: Literal["a", "r", "s", "d"] | None) -> object:
    """Convert the value to a string using the specified conversion."""
    # Python has no convert() built-in function, so we have to implement it.
    if conversion == "a":
        return ascii(value)
    if conversion == "r":
        return repr(value)
    if conversion == "s":
        return str(value)
    if conversion == "d": # This line is added for deferred evaluation support
        return value()
    return value

def f(template: Template) -> str:
    """Implement f-string behavior using the PEP 750 t-string behavior."""
    parts = []
    for item in template:
        match item:
            case str() as s:
                parts.append(s)
            case Interpolation(value, _, conversion, format_spec):
                value = convert(value, conversion)
                value = format(value, format_spec)
                parts.append(value)
    return "".join(parts)

This simple change naturally integrates the !d conversion into the existing system.

When Values Do Not Always Need Evaluation

Depending on the logging level set, log messages may not require evaluation. In such cases, the !d conversion can prevent unnecessary expression evaluation.

# If the logger level is set above DEBUG, expensive_func is not evaluated at all.
logger.debug(t"User stats: {expensive_func(val1, val2, flag=True)!d}")

When Values Change in Real Time

Another application of the !d conversion occurs when dealing with values that change in real time.

The following example retrieves a value from a specific API every second and logs it. Using the !d conversion both avoids unnecessary calculations when debug messages are not required, and permits the template string to be created once and reused repeatedly.

log_value = t"[{datetime.now().isoformat()!d}] current value is {value!d} (Attempt {i!d})"

for i in count(1):
    value = httpx.get("https://...").json()
    logger.debug(log_value) # datetime.now() and value are evaluated at the time the log is printed
    do_something(value)
    time.sleep(1)

The following example demonstrates periodically updating news headlines every minute in a hypothetical GUI library. In this implementation, the content of the text element is periodically updated through a deferred template string.

from gui import GUI
from news_api import get_headline

def main():
    window = GUI.create_window()
    text_element = window.add_element("text")
    # Fetch a new headline every 60 seconds.
    text_element.set_text(
        t"Today's News | {get_headline(section='economic')!d: <100} | {datetime.now().strftime('%H:%M')!d}",
        refresh_interval=60
    )
    window.start()

Relationship to Tagged String

PEP 750 originally proposed the concept of tagged strings. This approach deferred all interpolation by default. However, the PEP later evolved into proposing template strings, evaluated immediately in the same way as f-strings.

Unlike tagged strings, the !d conversion requires users to explicitly opt-in to deferred evaluation. This explicit opt-in approach also allows for more flexible control, as it can be applied only to specific interpolations that require deferred evaluation.