# PEP 750: Tag Strings For Writing Domain-Specific Languages

**URL:** <https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408>\
**Category:** PEPs\
**Created:** [August 9, 2024, 3:40pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408 "2024-08-09T15:40:07Z")\
**Posts on this page:** 20\
**Page:** 14

<div class="post-metadata">

**Author:** ![pf\_moore](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/pf_moore/32/35_2.png) [@pf\_moore](https://discuss.python.org/u/pf_moore)\
**Post date:** [October 24, 2024, 3:35pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/264 "2024-10-24T15:35:16Z")

</div>

> [@dkp](#):
>
> In the end, I feel we found a useful digestible increment. But the cuts were hard.

I agree. [PEP 735](https://www.python.org/dev/peps/pep-0735/) is another example of a proposal that started relatively large and got quite ruthlessly cut to produce a much more well-focused core proposal. It even included an explicit “Deferred Ideas” section listing potential future extensions. I think that lazy evaluation would make a very good addition to a “deferred ideas” section in this PEP (as opposed to classifying it as a “Rejected” idea).

> [@dkp](#):
>
> Another suggestion in this thread was to introduce a `!()` conv. It occurs to me that the current PEP might actually _preclude_ doing so in the future since `Interpolation.value` is pre-conversion, not post. Hrm.

If there’s a way to modify the PEP so that `!()` remains a potential future extension, that would be useful\[1\]. One advantage of having lazy evaluation as a “deferred idea” is that it clearly marks certain areas of the design space where we want to leave things flexible, _without_ committing to solving all of the design problems right now.

* * *

1. Not least because it’s the option I think I like the most 🙂

---

<div class="post-metadata">

**Author:** ![dkp](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/dkp/32/20413_2.png) [@dkp](https://discuss.python.org/u/dkp)\
**Post date:** [October 24, 2024, 3:46pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/265 "2024-10-24T15:46:05Z")

</div>

> [@pf\_moore](#):
>
> Deferred Ideas

“Deferred Ideas About Deferral” 😆

But in seriousness, that’s a good suggestion for the next round of edits. Thanks.

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [October 25, 2024, 4:08am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/266 "2024-10-25T04:08:34Z")

</div>

> [@dkp](#):
>
> (Another suggestion in this thread was to introduce a `!()` conv. It occurs to me that the current PEP might actually _preclude_ doing so in the future since `Interpolation.value` is pre-conversion, not post. Hrm.)

Making `!()` work was the _reason_ the last pre- withdrawal version of PEP 501 had lazy conversion, so I think you’re safe on that front.

Keeping that future syntax option available is likely worth mentioning as part of the rationale for lazy value conversion, though.

---

<div class="post-metadata">

**Author:** ![zuo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zuo/32/11039_2.png) [@zuo](https://discuss.python.org/u/zuo)\
**Post date:** [October 26, 2024, 8:51am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/267 "2024-10-26T08:51:54Z")

</div>

After some thought, I believe that the main problem with the proposal – and the cause of some reservations expressed in this thread (especially by @pf_moore and @MegaIng, if I properly understood the crux of your messages) – is that the term _ **template** _ promises some kind of _skeleton_ or _form_, or a _ **structure** _ – which can _ **be filled later with actual content** _.

In other words, I bet that many people, when hear about _ **template** strings_, will expect that their usage, once a _template string_ will be evaluated resulting in a _template object_, will then include the possibility to _fill that structure (template) with some content_, e.g.:

```python
template = t"Monty {animal} and the Holy {cup}"
filled = template.fill({"animal": "Python", "cup": "Grail"})
# and maybe only then:
html(filled) # etc.

```

Whereas the proposed syntax, with the associated types, offers just a way to **immediately define _structure_ and _content_** , which then (typically) is only meant to be completed with certain presentation qualities (such as DSL-specific escaping etc.).

* * *

What I am trying to express is that IMHO the feature is interesting and probably very useful, yet using the term _template_ to describe it would lead to misunderstanding and disappointments – at least unless it includes some actual _templating_ possibilities, i.e., some natural way to decouple defining _structure_ from defining _content_.

* * *

PS [EDIT] Let me clarify: the above code snippet is **not** something I propose, but something I am afraid people will expect when they hear about _ **template** strings_.

* * *

PPS [late EDIT] But, said all that, I’d like to emphasize that the proposal described in the point (3) of my [later post](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/287) (and in further posts following that one) could help greatly reduce the above issue.

---

<div class="post-metadata">

**Author:** ![Nodd](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/nodd/32/6805_2.png) [@Nodd](https://discuss.python.org/u/Nodd)\
**Post date:** [October 26, 2024, 9:55am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/268 "2024-10-26T09:55:25Z")

</div>

I agree, the term template is misleading. While following this thread, I have to constantly flip a mental switch to understand the behavior. Template is used for jinja-like tools, see [https://wiki.python.org/moin/Templating](https://wiki.python.org/moin/Templating)

---

<div class="post-metadata">

**Author:** ![zuo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zuo/32/11039_2.png) [@zuo](https://discuss.python.org/u/zuo)\
**Post date:** [October 26, 2024, 10:45am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/269 "2024-10-26T10:45:02Z")

</div>

To be more clear about what my suggestion is when it comes to my previous post…

I believe there are, generally, three options:

- to dismiss my reservations expressed in that post as inessential – i.e., to keep the feature and terminology as they are;

- to keep the feature as it is currently described in the PEP, but to change the terminology – including changing the class name `Template` (and probably also the `t` prefix) to something which would not suggest the _templating_ possibilities in the meaning pointed to in my previous post;

- to enrich the feature described in the PEP, by adding to it the _templating_ possibilities as meant in the previous post (i.e., the possibility to decouple defining _structure_ [what fields in which places] from defining _content_ [field values]) as a first-class citizen.

When it comes to the latter option, I would propose something what could be used like so:

```python
animal = "bird"
t1 = t"A {:animal_weight} {animal} could not carry a {:coconut_weight} coconut."
assert t1 == t"A {:animal_weight} {'bird'} could not carry a {:coconut_weight} coconut."
html(t1) # raises error ("unfilled fields: 'animal_weight', `coconut_weight`")

t2 = t1.fill(animal_weight="<5 ounce") # keyword arg.
assert t2 == t"A {'<5 ounce'} {'bird'} could not carry a {:coconut_weight} coconut."
html(t1) # raises error ("unfilled fields: `coconut_weight`")

t3 = t2.fill({"coconut_weight": ">1 pound"}) # mapping as pos. arg.
assert t3 == t"A {'<5 ounce'} {'bird'} could not carry a {'>1 pound'} coconut."
assert html(t3) = "A &lt;5 ounce bird could not carry a &gt;1 pound coconut."

```

Presumably, a new type would be introduced, `UnfilledField` – similar to `Interpolation` but without `value`, and with `name` instead.

The `fill()` method would accept a mapping as a single positional argument or (for convenience) any number of keyword arguments. Not all unfilled fileds would be required to be filled in one call to this method – which means that the desired content could be added _incrementally_.

There could also exist a similar method: `fillall()` – which would require that unfilled fields must be specified _all at once_ (i.e., that the resultant `Template` object must not include any `UnfilledField`s), or an error would be raised (`UnfilledTemplateError`, which could be a subclass of `ValueError`).

Rendering functions would be free to either accept `UnfilledField`s (and react appropriately to their specifics, e.g., providing some default values) or raise an error (as the example function `html()` does in the above code snippet; presumably `UnfilledTemplateError` would be suggested in such cases).

Note that with those `{:xyz}` _unfilled fields_ we would **not** introduce any lazy-evaluation mechanism, but just a possibility to explicitly provide (some of) the content **later** (not necessarily at the moment when the template string is evaluated and the _template structure_ is determined), possibly _incrementally_, and using a _first-class-citizen_ functionality – without resorting to unstandarized hacks (like escaping field delimiters by typing `{{` and `}}` or setting values to `...`).

* * *

EDIT: slightly different, yet generally improved and more “cross-sectional” proposals are in [my later post](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/279).

---

<div class="post-metadata">

**Author:** ![Nineteendo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/nineteendo/32/19122_2.png) [@Nineteendo](https://discuss.python.org/u/Nineteendo)\
**Post date:** [October 26, 2024, 11:08am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/270 "2024-10-26T11:08:07Z")

</div>

> [@zuo](#):
>
> to keep the feature as it is currently described in the PEP, but to change the terminology –

Maybe we could switch back to `InterpolatedString` or `InterpolationTemplate` which was used in an earlier version of pep 501:

> <https://github.com/python/peps/blob/49b59351903fba968b2672a7eb390f304a8ce4d6/peps/pep-0501.rst>

If we added “real” template strings, we could maybe do something like this:

```python
string_template = t"Hello {name}!"
interpolated_string = string_template.fill(name="World")
assert interpolated_string == i"Hello {"World"}!"
rendered_string = render(interpolated_string)
assert rendered_string == "Hello World!"

```

But I would that defer to a followup PEP, as it depends on this one.

---

<div class="post-metadata">

**Author:** ![Nineteendo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/nineteendo/32/19122_2.png) [@Nineteendo](https://discuss.python.org/u/Nineteendo)\
**Post date:** [October 26, 2024, 11:59am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/271 "2024-10-26T11:59:50Z")

</div>

And you would also be able to fill in names in the format specifier:

```python
assert t"Price: {price:{fmt}}".format(price=49, fmt=".2f") == i"Price: {49:.2f}"

```

---

<div class="post-metadata">

**Author:** ![zuo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zuo/32/11039_2.png) [@zuo](https://discuss.python.org/u/zuo)\
**Post date:** [October 26, 2024, 12:04pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/272 "2024-10-26T12:04:21Z")

</div>

> [@Nineteendo](#):
>
> And you would also be able to fill in names in the format specifier:
> 
> ```python
> assert t"Price: {price:{fmt}}".format(price=49, fmt=".2f") == i"Price: {49:.2f}"
> 
> ```

I would not provide such a method, which IMHO could appear to be an _attractive nuisance_.

Templates are _not_ strings. Let formatting their content be the job of _rendering functions_.

---

<div class="post-metadata">

**Author:** ![Nineteendo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/nineteendo/32/19122_2.png) [@Nineteendo](https://discuss.python.org/u/Nineteendo)\
**Post date:** [October 26, 2024, 12:09pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/273 "2024-10-26T12:09:55Z")

</div>

Oops, I meant this:

```python
assert t"Price: {price:{fmt}}".fill(price=49, fmt=".2f") == i"Price: {49:.2f}"

```

I was looking at the behaviour of `str.format()` and forgot to chance the name of the function:

```python
>>> "Price: {price:{fmt}}".format(price=49, fmt=".2f")
'Price: 49.00'

```

The string would still need to be rendered by a function (and there’s no need to escape format specifiers).

---

<div class="post-metadata">

**Author:** ![zuo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zuo/32/11039_2.png) [@zuo](https://discuss.python.org/u/zuo)\
**Post date:** [October 26, 2024, 12:33pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/274 "2024-10-26T12:33:04Z")

</div>

PS When it comes to the second of the options mentioned in my post, i.e.:

> [@zuo](#):
>
> - to keep the feature as it is currently described in the PEP, but to change the terminology

…I agree with @Nineteendo that _`interpolated <something>`_ is a good direction when it comes to finding a better term for the feature and the main type related to it.

Namely, I believe that i-strings (an `i"{something}..."` syntax, as initially proposed in PEP 501) and the `InterpolatedContent` type name (instead of t-strings and `Template`, respectively) would be a good choice. This way we would keep the door open for introducing “actual” _templates_ in the future – without the need to decide now whether we really need them (and what exactly their syntax and semantics should be if we decided to introduce them…).

By the way, to keep the terminology consistent with the existing conventions (especially, those used in the [f-string syntax’s docs](https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals) and the relevant portions of [the `string` module’s docs](https://docs.python.org/3/library/string.html#format-string-syntax)), I propose to rename the type `Interpolation` to `ReplacementField` – becase _replacement field_ is an established term to describe all those `{`-and-`}`-delimited portions of f-strings and `str.format()`-processed patterns. Note that also in numerous code examples already posted in this thread (e.g., by @ncoghlan) the name `field` was chosen quite often as a variable name referring to an instance of that type.

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [October 26, 2024, 2:06pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/275 "2024-10-26T14:06:09Z")

</div>

Since the switch from “interpolation templates” to “template strings” happened in PEP 501 rather than being new in PEP 750, I can give some background on it.

The most basic reason (and the one that PEP 501 cites) is that we’re intentionally using the same name as the comparable JS feature:

> **[Template literals (Template strings) - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)**
>
> Template literals are literals delimited with backtick (\`) characters, allowing for multi-line strings, string interpolation with embedded expressions, and special constructs called tagged templates.

The other reasons I accepted that change to PEP 501 when @nhumrich proposed it were:

1. it’s a simpler name and the shorthand form is easier to pronounce
2. The syntax still supports true templating (by putting an ellipsis literal in every field), it just also makes it easy to provide contents for the fields directly when it is useful to do so (and it is frequently useful to do so)

I admit I do sometimes think of them as “populated template strings” (vs the “bare template strings” that the string module offers), but they’re still template strings.

---

<div class="post-metadata">

**Author:** ![Nineteendo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/nineteendo/32/19122_2.png) [@Nineteendo](https://discuss.python.org/u/Nineteendo)\
**Post date:** [October 26, 2024, 4:42pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/276 "2024-10-26T16:42:11Z")

</div>

> [@ncoghlan](#):
>
> The syntax still supports true templating (by putting an ellipsis literal in every field)

While workarounds might be possible, I don’t think it’s a good idea to use a more verbose notation than `str.format[_map]()` which also doesn’t allow fields in format specifiers. We are better off adding a separate prefix to fulfil this need or new string method(s).

This PEP doesn’t need to solve everything, and it should just admit what’s impossible.

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [October 26, 2024, 11:40pm UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/278 "2024-10-26T23:40:19Z")

</div>

> [@Nineteendo](#):
>
> This PEP doesn’t need to solve everything, and it should just admit what’s impossible.

Everything described in [PEP 750: Tag Strings For Writing Domain-Specific Languages - #224 by ncoghlan](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/224) is possible with the PEP as written.

There are certainly rough edges in the dynamic templating use cases, but those use cases also _already have good solutions_ (like `jinja2` and `string.Formatter`).

So from a dynamic templating point of view, my recommended way to think of template literals is as a feature that may help make writing dynamic templating interfaces easier, and likely higher performance, and potentially offer some API design improvement opportunities, but doesn’t offer anything fundamentally new.

Instead, the most interesting use cases are those where the template is prepopulated with specific values that need to be treated with suspicion, such as subprocess command execution and SQL database queries, but the overall structure of the template itself is less likely to be data driven (unlike HTML).

This is why PEP 501 focused on the `shlex.sh` template processor as its primary motivation (and assuming PEP 750 is eventually accepted, @nhumrich and I intend to write a new PEP _just_ for adding `shlex.sh` as a standard template processor).

---

<div class="post-metadata">

**Author:** ![zuo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zuo/32/11039_2.png) [@zuo](https://discuss.python.org/u/zuo)\
**Post date:** [October 27, 2024, 12:01am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/279 "2024-10-27T00:01:52Z")

</div>

Putting aside the terminology/naming issue for a moment, I’ll now attempt to put together the building blocks from several previous posts by various authors (including me) plus some fresh ideas – to explore, and make it easy to compare, various possibilities to add _ **“true templating”** _ (i.e., _optional_ decoupling the step of defining the _template structure_ from the step of specifying the _content_, i.e., _actual interpolated values_) on top of the current design of the _template strings_ feature (possibly suggesting some non-fundamental changes to that design).

(Please, correct me if I omit any important elements or misrepresent somebody’s ideas.)

## Variant #1: _Ellipsis_-based

Assuming that the use of `...` (the _Ellipsis_ literal) as a special value denoting an _unfilled value_ (as suggested by @ncoghlan) is a satisfying syntax, we might want to use it in the following way:

```python
animal = "bird" # (<- only this field value will be specified immediately)
t1 = t"A {...:animal_weight} ounce {animal} could {...:neg!r} carry a {...:coconut_weight:.2f} pound coconut."
assert t1 == t"A {...:animal_weight} ounce {'bird'} could {...:neg!r} carry a {...:coconut_weight:.2f} pound coconut."
html(t1) # Could raise: ValueError("unfilled fields: 'animal_weight', 'neg', 'coconut_weight'")

t2 = fill(t1, animal_weight="<5", neg="not")
assert t2 == t"A {'<5'} ounce {'bird'} could {'not'!r} carry a {...:coconut_weight:.2f} pound coconut."
html(t2) # Could raise: ValueError("unfilled fields: 'coconut_weight'")

t3 = fill(t2, {"coconut_weight": 1})
assert t3 == t"A {'<5'} ounce {'bird'} could {'not'!r} carry a {1:.2f} pound coconut."
assert html(t3) = "A &lt;5 ounce bird could &#39;not&#39; carry a 1.00 pound coconut."

```

(Please note how conversions and format specs are supported…)

The `fill()` [in some of my previous posts I proposed a less suitable name, `bind`] function would need to be implemented somehow along the lines of the following:

```python
def fill(
    template: Template,
    mapping: Mapping[str, Any] | None = None,
    /,
    **kwargs: Any,
) -> templatelib.Template:

    if mapping is None:
        mapping = kwargs
    elif kwargs:
        raise TypeError(
            "fill() takes either a mapping as the "
            "sole positional argument or any number "
            "of keyword arguments, but not both")

    match_spec_regex = _FILL_SPEC_REGEX.fullmatch # (see below)

    def _gen_segments() -> Iterator[str | Interpolation]:
        for segment in template.args:
            match segment:
                case Interpolation(bultins.Ellipsis, "...", noconv, spec):
                    match = match_spec_regex(spec)
                    if match is None:
                        raise ValueError(
                          f"when `...` is used as a placeholder for "
                          f"a value to be filled-in, what needs to "
                          f"be placed to the right of the `...:` "
                          f"marker is the obligatory name part with "
                          f"an optional conversion and/or format "
                          f"specification (got: {spec!r})")
                    if noconv is not None:
                        raise ValueError(
                          "when `...` is used as a placeholder for "
                          "a value to be filled-in, the conversion "
                          "part, if any, needs to be placed to the "
                          "right of the name part (which follows "
                          "the `...:` marker)")
                    field_name = match["field_name"]
                    if not field_name.isidentifier():
                        # (keep it simple + avoid parsing ambiguities)
                        raise ValueError(
                          f"when `...` is used as a placeholder for "
                          f"a value to be filled-in, the name part "
                          f"needs to be a valid Python variable "
                          f"name (got: {field_name!r})")
                    value = mapping[field_name]
                    expr = repr(value) # Or match["field_name"] ?!?
                    conv = match["conv"]
                    format_spec = match["format_spec"] or ""
                    yield Interpolation(value, expr, conv, format_spec)
                case Interpolation() | str():
                    yield segment
                case _:
                    typing.assert_never(segment)

    return Template(*_gen_segments())

_FILL_SPEC_REGEX = re.compile(
    r"(?P<field_name>[^!:]+)"
    r"(?:"
        r"!" # (no need to be strict here,
        r"(?P<conv>[^:]+)" # as Interpolation() constructor
    r")?" # will validate it anyway...)
    r"(?:"
        r":"
        r"(?P<format_spec>.*)"
    r")?"
)

```

Perhaps that function could become a `Template`’s instance method, as the _blessed_ way to process `...:`-engaging templates? (not necessarily immediately, maybe in another major release of Python…)

Anyway, with this approach “true templating” can be easily added on top of the current implementation of _template strings_.

## Variant #2: _`tt`-strings_ syntax

Another approach would require some syntax extension…

One possible way to denote an _unfilled_ (aka _unbound_) replacement field could be the `{:name}`-like syntax (proposed in one of my latest posts). However, now I think it is hardly better than the `{...:name}`-based approach described above (as being not much prettier than the latter one, which at least makes occurrences of _unfilled fields_ more visible/distinguishable…).

So here I’d like to explore yet another variant (important features of which were suggested by @Nineteendo) which, as it seems, could be introduced later, in a separate PEP. To do that it would be required to add:

- **`tt`-strings** – with the semantics drafted below (obviously some other prefix than `tt` might be used [e.g., `t` if it was decided that the base prefix is `i`…]),

- a new exception type: `UnfilledTemplateError` (being a subclass of `ValueError`),

- a new type (somewhat similar to `Interpolation`):

E.g., we might want to use that stuff in the following way:

```python
# Just for more consistent naming in the following examples:
ReplacementField = Interpolation  

# First, a well known PEP-750 template string:
a = "<5"
b = "not"
c = 1
immediate = t"A {a} bird could {b!r} carry a {c:.2f} pound coconut."
assert isinstance(immediate, Template) and immediate == Template(
    "A ",
    ReplacementField("<5", "a", None, ""),
    " ounce bird could ",
    ReplacementField("not", "b", "r", ""),
    " carry a ",
    ReplacementField(1, "c", None, ".2f"),
    " pound coconut.",
) # Typically also equivalent (though not equal) to:
   # t"A {'<5'} ounce {'bird'} could {'not'!r} carry a {1:.2f} pound coconut."

# Now, the new stuff...
t1 = tt"A {bird_weight} ounce bird could {neg!r} carry a {coconut_weight:.2f} pound coconut."
assert isinstance(t1, Template) and t1 == Template(
    "A ",
    UnfilledField("bird_weight", None, ""),
    " ounce bird could ",
    UnfilledField("neg", "r", ""),
    " carry a ",
    UnfilledField("coconut_weight", None, ".2f"),
    " pound coconut.",
) != immediate
html(t1) # Could raise: UnfilledTemplateError("unfilled fields: 'bird_weight', 'neg', 'coconut_weight'")

t2 = t1.fill(bird_weight="<5", neg="not")
assert isinstance(t2, Template) and t1 != t2 == Template(
    "A ",
    ReplacementField("<5", "bird_weight", None, ""),
    " ounce bird could ",
    ReplacementField("not", "neg", "r", ""),
    " carry a ",
    UnfilledField("coconut_weight", None, ".2f"), # <- Note: still unfilled!
    " pound coconut.",
) != immediate
html(t2) # Could raise: UnfilledTemplateError("unfilled fields: 'coconut_weight'")

t3 = t2.fill({"coconut_weight": 1})
assert html(t3) == html(immediate) == (
    "A &lt;5 ounce bird could &#39;not&#39; carry a 1.00 pound coconut."
)
# Note: typically, `t3` is also equivalent to `immediate`
# (though not equal to it -- because of `expr`s).

```

`Template.fill()` (somewhat similar to the `fill()` function from _Variant #1_) would accept one positional argument (a mapping) or any number of keyword arguments, and would produce a new instance of `Template` – with selected `UnfilledField` instances replaced with appropriately constructed `Interpolation` ones (where _appropriately constructed_ means: `Interpolation(mapping[unfilled.name], unfilled.name, unfilled.conv, unfilled.format_spec)`).

`Template` would also provide the following methods:

- `Template.fill_all()` – similar to `fill()`, but raising `UnfilledTemplateError` if any `UnfilledField` is remaining;
- `Template.all_fields_filled()` – returning `True` unless `args` contains any `UnfilledField`;
- _classmethod_ `Template.make_from()` – accepting a template as an ordinary `str`, plus optionally such argument(s) as accepted by `Template.fill()`; returning a `Template` whose `args` contains appropriate string/`Interpolation`/`UnfilledField` segments (as appropriate).

## Variant #3: _`tt`-strings_ syntax with `Template` tweaks

Like _Variant #2_ – but with modified `Template`, so that:

- the constructor `Template()` behaves like `Template.make_from()` from _Variant #2_;
- the type has _classmethod_ `Template.from_segments()` – behaving like the current `Template()` constructor;
- the type does _not_ have `Template.make_from()`, as it would be redundant (see above).

## Variant #4: with a _factory maker_

In this variant we would have neither any additional syntax nor _Variant #1_-like extra conventions and tools.

Instead, the `Template` class would provide only one additional _classmethod_: `Template.get_factory()`; it would accept a template as an ordinary `str` (like the first argument to `make_from()` in _Variant #2_) as well as, optionally, the default values for any subset of the template’s fields (in such a form as for `Template.fill()` in _Variant #2_); it would return a _callable_ which:

- would have to be called with argument(s) (in such a form as for `Template.fill()` in _Variant #2_) specifying values for _at least_ those fields which were not included when `get_factory()` was called to obtain the _callable_;
- would return a `Template` instance whose `args` contains appropriate string/`Interpolation` segments as appropriate (given the arguments).

Exampe use:

```python
make_grail_template = Template.get_factory(
    "A {animal_weight} ounce {animal} could {neg!r} "
    "carry a {coconut_weight:.2f} pound coconut.",
    animal="bird",
    neg="never",
)
assert isinstance(make_grail_template, Callable)
grail_template = make_grail_template(
    animal_weight="<5",
    neg="not", # (<- overrides the default from call to `get_factory()`)
    coconut_weight=1,
)
assert isinstance(grail_template, Template) and grail_template == Template(
    "A ",
    Interpolation("<5", "bird_weight", None, ""),
    " ounce bird could ",
    Interpolation("not", "neg", "r", ""),
    " carry a ",
    Interpolation(1, "coconut_weight", None, ".2f"),
    " pound coconut.",
) # Typically also equivalent (though not equal) to:
   # t"A {'<5'} ounce {'bird'} could {'not'!r} carry a {1:.2f} pound coconut."

```

_Note:_ this variant, by itself, would not provide the possibility to fill field values _incrementally_ (though that could be emulated with `functools.partial`).

## Combined Variants: #1-with-#4, #2-with-#4, #3-with-#4

Combining any of the variants _#1/#2/#3_ with _Variant #4_: the _callable_ produced by the _classmethod_ `Template.get_factory()` accepts values for any subset of the template’s fields (i.e., _not_ necessarily for all fields omitted when `get_factory()` was called), and returns a new `Template` with replacement fields _filled_ and/or _unfilled_, as appropriate.

## Conclusion

It seems that each of the “true templating” variants described above could be added on top of the current design of _template/interpolation strings_ (some of them with certain non-essential modifications to that design).

Any thoughts? 🙂

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [October 27, 2024, 2:01am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/280 "2024-10-27T02:01:30Z")

</div>

> [@zuo](#):
>
> It seems that each of the “true templating” variants described above could be added on top of the current design of _template/interpolation strings_ (some of them with certain non-essential modifications to that design).
> 
> Any thoughts?

Your conclusion is the same as mine: there are a variety of ways we _could_ improve the dynamic templating support, but it’s far from clear which of them would be the best path to take. (One simple variant that occurred to me was to have `{...}` and `{}` literally mean the same thing in template strings: if you omit the expression entirely, it is implicitly `Ellipsis`)

As long as PEP 750 doesn’t _prevent_ pursuing those ideas later, then the situation feels similar to the one PEP 501 was in back when f-strings were first proposed: experience gained with the narrower proposal will help improve the expanded proposal, so waiting is a good option.

In that vein, one possibility we may want to revisit is to reintroduce a structural typing protocol for the `Template` interface. The protocols were taken out because we didn’t see a strong use case for them (and I still don’t see a strong use case for an `Interpolation` protocol), but I’m now wondering if it might be worth our while to start with this initial structure:

- `templatelib.Template` (protocol)
- `templatelib.StaticTemplate` (concrete PEP 750 template)
- `templatelib.Interpolation` (concrete PEP 750 field)

Which would then make experimentation with dynamic templates outside the standard library easier (no need to inherit from the concrete type, just implement the structural protocol), and leave the door open to the future addition of:

- `templatelib.DynamicTemplate` (concrete template optimised for dynamic value insertion)

Dynamic templates would still use the same interpolation field implementation as static templates, the meaning of their `value` fields would just be slightly different (either all `Ellipsis` to indicate their use as placeholders, or else holding default values to use when fields aren’t supplied explicitly)

Edit: Some other potential bikeshed colours for the PEP 750 concrete implementation type would be `BoundTemplate`, `FilledTemplate`, `PopulatedTemplate`. Essentially acknowledging that there’s a separation between the general concept of interpolation templates (the protocol), and the specific implementation backing t-strings (which produces already populated templates with default values for every replacement field)

---

<div class="post-metadata">

**Author:** ![zuo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zuo/32/11039_2.png) [@zuo](https://discuss.python.org/u/zuo)\
**Post date:** [October 27, 2024, 2:26am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/282 "2024-10-27T02:26:49Z")

</div>

Would the `Template` protocol cover the constructor? (That could preclude some future ideas…)

Another thing that seems to me worth deliberating: adding new methods to protocols is painful when it comes to backward compatibility (often the only option is to add a protocol’s subtype)…

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [October 27, 2024, 4:37am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/283 "2024-10-27T04:37:30Z")

</div>

> [@zuo](#):
>
> Would the `Template` protocol cover the constructor? (That could preclude some future ideas…)

Protocols can’t currently constrain the default constructor (see [Enforce compatible \_\_init\_\_ in structural subtypes · Issue #3823 · python/mypy · GitHub](https://github.com/python/mypy/issues/3823)).

However, as per [Protocols — typing documentation](https://typing.readthedocs.io/en/latest/spec/protocol.html#protocol-members), they _can_ require that certain class methods be available:

> Static methods, class methods, and properties are equally allowed in protocols.

This means a `Template` protocol could require that template implementations offer two standard alternate constructors (with useful default implementations):

```python
@classmethod
def from_segments(cls, *args: LiteralStr|Interpolation) -> Self:
    return cls(*args)

@classmethod
def from_template(cls, t:Template) -> Self:
    return cls.from_segments(t.args)

```

> [@zuo](#):
>
> Another thing that seems to me worth deliberating: adding new methods to protocols is painful when it comes to backward compatibility (often the only option is to add a protocol’s subtype)…

This is an area where adding `templatelib` as dedicated support module is useful: general purpose algorithms that are valid for _any_ `Template` implementation can live there, with only the parts that may be tightly coupled to a specific template implementation needing to be methods on the type.

---

<div class="post-metadata">

**Author:** ![Nineteendo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/nineteendo/32/19122_2.png) [@Nineteendo](https://discuss.python.org/u/Nineteendo)\
**Post date:** [October 27, 2024, 8:35am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/284 "2024-10-27T08:35:32Z")

</div>

> [@ncoghlan](#):
>
> Everything described in [PEP 750: Tag Strings For Writing Domain-Specific Languages - #224 by ncoghlan](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/224) is possible with the PEP as written.

This is not entirely equivalent to `str.format_map()`:

```python
>>> "Price: {price:{fmt}}".format_map({"price": 49, "fmt": ".2f"})
'Price: 49.00'
>>> template_from_format_map("Price: {price:{fmt}}", {"price": 49, "fmt": ".2f"})
['Price: ', (49, 'price', None, '{fmt}')]

```

But, you’re right that this could be implemented by a third party library.

* * *

Here’s my attempt at a summary of the different proposals (assuming fields in the format specifier can be replaced):

```python
         "Price: { :{ }}" .tformat( 49, ".2f")
Template("Price: { :{ }}" , 49, ".2f")
       tt"Price: { :{ }}" .fill ( 49, ".2f")
         "Price: { price :{ fmt }}" .tformat(price=49, fmt=".2f")
Template("Price: { price :{ fmt }}" , price=49, fmt=".2f")
       tt"Price: { price :{ fmt }}" .fill (price=49, fmt=".2f")
 Binder(t"Price: { 'price':{{fmt}}}").bind (price=49, fmt=".2f")
        t"Price: {...:price :{{fmt}}}" .fill (price=49, fmt=".2f")

```

---

<div class="post-metadata">

**Author:** ![zuo](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/zuo/32/11039_2.png) [@zuo](https://discuss.python.org/u/zuo)\
**Post date:** [October 27, 2024, 9:46am UTC](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408/285 "2024-10-27T09:46:20Z")

</div>

What I intended to emphasize here was that (given that protocols and ABCs tend to be “carved in stone” once officially defined) it might be worth wondering whether, for now, woudn’t it be more safe to rely on an informal duck-typing-based expectations (explicitly documented – but including the caveat of being open to further extensions) rather than on a formal protocol definition; with the intent, that the latter will be formalized in a future version.

[Previous page](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408.md?page=13)

[Next page](https://discuss.python.org/t/pep-750-tag-strings-for-writing-domain-specific-languages/60408.md?page=15)
