I don’t recall it either. I think if separate templates and values were the main API, it would be seen as unnecessary complexity for most use cases.
But for libraries that do a lot of work for a template (e.g. parsing HTML), it’s good to be able to cache that work. The current architecture makes that less simple, but still quite possible: extracting the values from one template and using them with the processed version of the other is quite doable.
This way we’re making simple things simple and hard things possible.
Agreed, and I think we could add an UnboundTemplate and UnboundInterpolation types later without having to implement the bound Template type in terms of them (providing a way of getting the unbound template corresponding to a bound template yes, actually implementing it that way, not necessarily).
That design process would be easier to do given concrete example of folks rolling their own unbound template equivalents based on the initial bound template design, so it still feels like a “later” problem to me.
Thinking of the “What does template equality mean?” problem that way is still pushing me towards favouring native hashing and equality being identity based, and offering some kind explicit “cache key” API that extracts the invariant parts of the template instance:
the strings tuple
the expr, conv, and format_spec parts of the interpolation fields tuple
Such a cache key API would presumably also be usable for the currently equality-based test cases that @dkp mentioned (for cases that care about field values, the string and interpolation field tuples can be compared directly).
It shouldn’t be too complex to move template.args[*].value into a dict keyed off template.args[*].expr, right? That should be enough for template to be static[1] and context to vary:
def lower_upper(template: Template, context: dict[str, Any]) -> str:
"""Render static parts lowercased and interpolations uppercased."""
parts: list[str] = []
for arg in template.args:
if isinstance(arg, Interpolation):
parts.append(str(context[arg.expr]).upper())
# previously: parts.append(str(arg.value).upper())
else:
parts.append(arg.lower())
return "".join(parts)
If we really wanted, we could add a for arg in template.with_context(context): that yields args with .value set, though I think given the alternating sequence complexity we’re not really making things worse.
As in, the identical template object is passed to the processor each time. ↩︎
One instinct that guided our current definition of equality: asserts with t-strings should generally run parallel to those with f-strings. But over time, I’ve decided that this instinct is bad/misleading. No matter howInterpolation.__eq__ is defined, we can concoct assert divergences and then scratch our heads about whether those divergences will be surprising to devs. Also, no single definition feels obviously “natural” and avoids potential for developer confusion (as in the current PEP, where Template.__hash__ is confusing and not useful as a cache key).
Best perhaps to treat Template and Interpolation as types that are quickly processed into something more semantically meaningful; developers can define equality, etc. on those derived types if they wish. And test code can make its own decisions.
I could see doing this, or just leaving it as a problem for another day: let devs decide if strings is sufficient for caching, or whether they need more, and provide a future API if it’s useful.
Ah, very good point. A third parallel tuple is getting too complex for my liking - I’d rather templates just have a .key argument at that point. I feel like treating the entire template object as a key will lead people to assume the substitutions are all the same and cache the result, not a partially processed string.
There were a couple suggestions about naming in this (and the previous) discussion thread:
Consider renaming Interpolation to Field (and .interpolations to .fields)
Consider renaming .strings to .literals (with an eye towards future bytes templates)
I don’t feel strongly but if I were forced to bikeshed I might choose “Field” – less typing! – and probably would keep .strings because I’m old and think about byte strings anyway.
I worked with Dave on some of the changes and the __iter__ made the examples fun, to just iterate over the template.
We will likely start the process of getting on the steering council’s radar, as we’ve integrated multiple rounds of review and feedback. A big thanks to Dave for all the editing and replying, shepherding this for the four or five months.
Reading through this, what would surprise me whilst using it is that str(template) would not evaluate to what is written as f(template) in the PEP.
Regardless, the PEP looks useful and would not bother me.
I expected that someone had proposed defining __str__() that way, someone else had made a good point why another __str__() method could be better, and not defining __str__() was the compromise. But I can’t find any such discussion. Did I look in the wrong place, or did no-one suggest defining how str should act on Templates?
Yup, it’s been discussed. You’re right that there’s no “natural” __str__ although at a glance f() feels it could be a good choice. One concern is that a common use of template strings will be to avoid injection attacks (SQL injection, XSS, etc); if str(template) behaves like f(), it might be easy for devs to inadvertently write vulnerable code. On the other hand, if __str__ just returns the repr, it’ll be easy to spot that a given template wasn’t processed (for instance, by calling sql() or html()).
The authors suggest that a Mechanism to Describe the “Kind” of Template could be developed and grow. Can they give examples of how this might be approached?
We punted in the PEP because (a) it broadened the scope too far and (b) we never found an answer we were wholly satisfied with.
But: something like Annotated[Template, "html"] feels like the place to start. One could imagine black and ruff using these annotations to format template strings, or mypy and pyright using them to check interpolation types.
One thing that would improve static typing would be to make Template and Interpolation generic over the values. That way functions could declare what values are valid for them to interpolate. Since it’s readonly, it’ll be covariant so a default of Template[object] would cover any possible value, no need for Any. For instance, the HTML formatting use case could require only formatting other elements into the tree. Obviously this is defined in the stubs, but it’d be necessary to make it generic at runtime also.
The Python Steering Council has been reviewing PEP 750 in detail over the past several meetings. We are want to inform you that we intend to accept this PEP, pending some minor changes that we believe will strengthen and clarify the proposal.
While we are still finalizing the formal acceptance notice with specific recommendations, we wanted to communicate our positive decision early to help you plan accordingly. The full acceptance notice with detailed change requests will be published in the coming days.
We appreciate the thorough work you’ve put into this proposal and the extensive community discussion it has generated. The many iterations, expressions of support, feedback and contributions from community members have strengthened this PEP considerably, and we believe this feature will be a valuable addition to the Python language.
Thank you for your patience during our review process, and we look forward to working with you on the final refinements to PEP 750.
Pre-congratulations
Sincerely, Pablo in representation of the Python Steering Council
Do you see it landing in 3.14 or 3.15? Suppose Lysandros has a branch where it all works perfectly, and he could make the changes suggested by the SC quickly. Would that be acceptable? He won’t make tomorrow’s alpha, but there’s a final one April 8. Would you be okay with that?
On our side, assuming that the changes requested are not too big, we should be able to make it for a7 on April 8. Worst case scenario, if everyone’s okay with it, we could also get it in after the final alpha and before the beta freeze on May 6.