object: if conversion == "a": return ascii(value) elif conversion == "r": return repr(value) elif conversion == "s": return str(value) return value The built-in format function makes it easy to process the format specification for a replacement field, but there’s no built-in c...">

Add convert() function to string.templatelib (original) (raw)

I have been playing with t-strings and I keep finding myself copy-pasting the convert function from the PEP:

def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object:
    if conversion == "a":
        return ascii(value)
    elif conversion == "r":
        return repr(value)
    elif conversion == "s":
        return str(value)
    return value

The built-in format function makes it easy to process the format specification for a replacement field, but there’s no built-in convert function for handling !a, !r, and !s conversions.

It seems like this function might be useful to have implemented within string.templatelib, so it won’t need to be re-implemented by all t-string parsing utilities.

@dkp

AA-Turner (Adam Turner) June 5, 2025, 10:29pm 2

I support this idea, I would also suggest asking the RM to add this to 3.14, if accepted.

A

ilotoki0804 (Ilotoki0804) June 6, 2025, 1:06am 3

We might also consider implementing it as a staticmethod of Interpolation.

blhsing (Ben Hsing) June 6, 2025, 1:16am 4

Just curious though… It sounds like you’re trying to emulate an f-string with a t-string using format and convert, so then for what purpose are you not using an f-string directly?

I do kind of support the idea because it would be useful if/when the logging library supports t-strings as repr is useful primarily for debugging, but just want to ask for clarification of what the OP sees as use cases because I personally don’t quite see many other scenarios where a template library wouldn’t want to always call str on the interpolation’s value if stringification is needed so I doubt that the claim that convert “needs to be re-implemented by all t-string parsing utilities” is remotely true (for example, the talks about adding t-string support for subprocess and DB-API don’t include a mention of the standard convert logics).

trey (Trey Hunner) June 6, 2025, 2:21am 5

This was for this library I made today.

The dedent function in that library uses a t-string in a very similar way to an f-string, but delaying interpolation is important for its purpose.

blhsing (Ben Hsing) June 6, 2025, 2:57am 6

Right, the primary use case then is for a library to emulate f-strings while delaying the evaluation of the interpolation’s stringification. Fair enough.

dkp (Dave Peck) June 6, 2025, 2:19pm 7

I’d like to see this too.

Would we prefer it:

  1. As a function directly in templatelib or
  2. As a staticmethod on Interpolation?

Would our preference if shipped be for it to:

  1. Accept a conversion of None as in the code above, or
  2. Only accept Literal[“a”, “r”, “s”] and always return a str?

Related: in past discussions there was talk of also adding an Interpolation.format_value(…) convenience method that invokes both convert() and the format() builtin in one go. It might make sense to do both at the same time.

AA-Turner (Adam Turner) June 6, 2025, 2:50pm 8

I think it should be a module-level function, we could see DSLs that use the conversion flag for entirely different things than formatted string literal-like behaviour if we relax the restriction on conversion flag values in the future. I think it should accept None, though, because that’s the default for an omitted conversion flag.

A

pR0Ps (Carey Metcalfe) June 6, 2025, 4:07pm 9

Unless I’m missing something, can’t the existing string.Formatter.convert_field already be used for this?

As an example, I’ve implemented a function to format a t-string like an f-string like so:

from string import Formatter
from string.templatelib import Template, Interpolation

_FORMATTER = Formatter()

def tstring_as_fstring(tstring):
    return "".join(
        (
            _FORMATTER.format_field(
                _FORMATTER.convert_field(x.value, x.conversion),
                x.format_spec
            )
        ) if isinstance(x, Interpolation)
        else x
        for x in tstring
    )

Also of note is that the string.Formatter.convert_field method doesn’t actually use the bound self arg so it’s possible to implement your function like:

import functools
from string import Formatter

convert = functools.partial(Formatter.convert_value, None)

# or, more verbosely:

def convert(value: object, conversion) -> object:
    return Formatter.convert_value(None, value, conversion)

Though it’s probably less likely to break if it’s called “properly” via an actual Formatter instance like the first example vs. treating it like a static method.

AA-Turner (Adam Turner) June 6, 2025, 5:16pm 10

True, we could suggest that people alias convert = classmethod(string.Formatter.convert_field). My slight argument for putting it in string.templatelib is discoverability and semantics, though.

A

trey (Trey Hunner) June 6, 2025, 5:52pm 11

I hadn’t known about Formatter.convert_field.

I found that I needed to use either:

from functools import partial

convert = partial(Formatter.convert_field, Formatter)

Or (just classmethod didn’t work since that provided the descriptor but not the bound method):

from string import Formatter

convert = classmethod(Formatter.convert_field).__get__(Formatter)

gerardw (Gerardwx) June 6, 2025, 6:24pm 12

I also cribbed the PEP code, so having an Interpolation method makes sense to me.

from string.templatelib import Interpolation
from typing import Literal


# dervied from https://raw.githubusercontent.com/davepeck/pep750-examples/refs/heads/main/pep/fstring.py
def iformat(interp: Interpolation) -> str:
    """Format an interpolation like an f-string: apply conversion then format specifier."""
    value = interp.value
    conv = interp.conversion

    # apply conversion (ascii, repr, str) if specified
    if conv == "a":
        value = ascii(value)
    elif conv == "r":
        value = repr(value)
    elif conv == "s":
        value = str(value)
    # None or other means leave value as is

    # apply the format specifier
    return format(value, interp.format_spec)

yoavrv (Yoav Ravid) June 6, 2025, 10:05pm 13

As someone newer to python, I feel there should be an f-string syntax (as it’s the most obvious way to format strings in modern python).

I hoped the inner interpolation method from :format_spec would work but it doesn’t seem to work for the !conversion part.

I.e. This is a somewhat common idiom

formatted_value = f'{value:{format_spec}}'

But this doesn’t work

formatted_value = f'{value!{conversion}:{format_spec}}'

Personally I would prefer some kind of f-string version to a stand-alone function on nice-syntax and discoverability grounds, but I guess this would require changes to f-strings which is a whole PEP at least.

(Also this makes me think that maybe the Interpolation fields should default to "" rather than None but that ship has sailed)