Repository navigation
Consider making Template and Interpolation generic at runtime #133970
Description
Feature or enhancement
Template and Interpolation will once be supported by type-checkers. And it might be worth adding generics support for these types.
Example
from string.templatelib import Template, Interpolation
from urllib.parse import quote_plus
domain = 'example.com'
query = 'python string formatting is too complex'
template = t'https://{domain}?q={query}'
def quote_url(interp: Interpolation[str]) -> str:
return quote_plus(interp.value)
def format_url(template: Template) -> str:
parts = []
for part in template:
match part:
case str() as s: # regular string
parts.append(s)
case Interpolation(str(), expression='query') as interp:
parts.append(quote_url(interp))
case Interpolation(value):
parts.append(value)
return ''.join(parts)
print(format_url(template))Here we can see that Interpolation[str] can be useful in users' code.
We can also benefit from generic Template, where we can make it generic based on exact TypeVarTuple. Demo:
from typing import Any, TypeVarTuple, Generic, Unpack
Ts = TypeVarTuple('Ts', default=Unpack[tuple[Any, ...]])
class Template(Generic[*Ts]):
def __new__(cls, *args: *Ts) -> Template[*Ts]: ... # type: ignore[empty-body]
class Interpolation: ...
reveal_type(Template('a', 'b', Interpolation(), 'd'))
# Revealed type is "__main__.Template[Literal['a']?, Literal['b']?, __main__.Interpolation, Literal['d']?]"https://mypy-play.net/?mypy=latest&python=3.13&gist=0dc13b3b926e1efb9783ab9b70d39ceb
This can potentially help type checkers to infer correct Template type.
Current state
Current typeshed definitions: https://github.com/python/typeshed/blob/main/stdlib/string/templatelib.pyi
Here's how Template is defined:
@final
class Template: # TODO: consider making `Template` generic on `TypeVarTuple`
strings: tuple[str, ...]
interpolations: tuple[Interpolation, ...]
def __new__(cls, *args: str | Interpolation) -> Template: ...
def __iter__(self) -> Iterator[str | Interpolation]: ...
def __add__(self, other: Template | str) -> Template: ...
@property
def values(self) -> tuple[Any, ...]: ... # Tuple of interpolation values, which can have any typeIdeally, it should be generic on TypeVarTuple
Here's how Interpolation is defined:
@final
class Interpolation:
value: Any # TODO: consider making `Interpolation` generic in runtime
expression: str
conversion: Literal["a", "r", "s"] | None
format_spec: str
__match_args__ = ("value", "expression", "conversion", "format_spec")
def __new__(
cls, value: Any, expression: str, conversion: Literal["a", "r", "s"] | None = None, format_spec: str = ""
) -> Interpolation: ...Ideally, it should be generic on value. Interpolation[str] means that value is str and Interpolation[tuple[int, int]] means that Interpolation has a value for type tuple[int, int]
Proposal
From runtime's part with only need to add Py_GenericAlias, that's it.
Is it too late for feature freeze? This is a minor detail, so I hope it is not :)
I have a PR ready.
CC @srittau @JelleZijlstra @lysnikolaou
Activity
srittau commentedon May 13, 2025
One alternative is to make
Templategeneric over the interpolation value types instead. So__new__would remain typed as it is, but type checkers could special case this (which they already have to do to support template strings) so that:sobolevn commentedon May 13, 2025
@srittau this would be also possible, if we just add
Py_GenericAliastoTemplate. Exact typing would be determined later by type-checkers / typing council.JelleZijlstra commentedon May 13, 2025
Yes, I think we should do this, and it should be fine to do it during the beta phase as part of stabilizing the new feature.
We'll have to decide in typeshed what the actual generic parameters are. I think it makes the most sense to use a single type parameter for both
TemplateandInterpolation, so that for exampleTemplate[str]holds a series ofInterpolation[str]and each has a value of typestr.sobolevn commentedon May 13, 2025
@JelleZijlstra ok, I will send a PR with
Py_GenericAlias.pythongh-133970: Make PEP750 types generic
AA-Turner commentedon May 13, 2025
cc @lysnikolaou @davepeck (edit: Lysandros was alredy tagged, sorry!)
AA-Turner commentedon May 13, 2025
I think having
Interpolation[T], whereTis the type ofi.valuemakes sense. ForTemplate, my first reaction is thatt'a {1} b {dict()} c {0.0}'could be type-hinted asTemplate[LiteralString, int, LiteralString, dict, ...]. This is very verbose though, and it would probably be annoying to type! Jelle's suggestion of a single parameter may be simpler in practice, though more restrictive for heterogeneous types.What do the question marks mean here? This isn't legal syntax.
A
sobolevn commentedon May 13, 2025
@AA-Turner
This is how mypy pretty-prints type when
reveal_typeis called.?means that this type was inferenced, mostly.davepeck commentedon May 14, 2025
Considering the two suggested approaches:
Template[T]implies thatTemplate.interpolationsis typed astuple[Interpolation[T], ...]: I worry that real-world templates are nearly always heterogeneous and we'll end up with big unions.Template[A, B]impliesTemplate.interpolationsis typed astuple[Interpolation[A], Interpolation[B]]: The types seem likely to get very large. Consider, say, building a full HTML page from a deeply nested collection of templates.Walking through a simple example:
In this example:
{attribs}is adict[str, str | bool]{user.name}is astr{user.star_count}is anint{user_image(...)}is a nestedTemplate{classes}islist[str]In approach (1) we probably end up with
Template[object]in practice?In approach (2) I suppose we get something like
Template[dict[str, str | bool], str, int, Template[str, str, list[str]]]?JelleZijlstra commentedon May 14, 2025
I would think of this in terms of the type that you write if you accept a template. In your example, there might be a function
render_html(template). If it can interpolate saystrandintin a template, it would be typed asdef render_html(template: Template[str | int]):. Type checkers can then tell you if you pass some object that isn't allowed as a template value.4 remaining items
TeamSpen210 commentedon May 15, 2025
The big unions might be alleviated a bit by aliases, especially if checkers can display them unexpanded. Probably they'll be necessary anyway, since you'd want to specify sub-templates recursively. For the default,
objectmight be better - templates are immutable and therefore covariant, so that'll accept any template, but require users to verify interpolation values before use. It's inconsistent with other generics though...gh-133970: Make PEP750 types generic (#133976)
pythongh-133970: Make PEP750 types generic (pythonGH-133976)
sobolevn commentedon May 15, 2025
I merged the runtime generics PR, but I won't close the issue, we can continue talking about semantics here :)
[3.14] gh-133970: Make PEP750 types generic (GH-133976) (#134029)
lysnikolaou commentedon May 15, 2025
I'm not sure about this. Yes,
Interpolationbeing generic makes sense, butTemplateis much harder and both choices that have been presented here are far from ideal and will mostly go unused in practice. I think we need to think bigger about this and, while I'm not the right person to do it, we might also want to explore options for how our type system could evolve around t-strings and the new options they open up.Tying ourselves to this specific design of making
Templategeneric and then having to adhere to backwards-compatibility rules is not really something I'd like us to do. Maybe we should revert the PR and do it only forInterpolationfor now?JelleZijlstra commentedon May 15, 2025
The runtime support is very simple; it just allows writing
Template[whatever]creating atypes.GenericAlias. It's up to type checkers and typeshed to decide what exactly will be allowed within the brackets, and we can decide that later without impacting backwards compatibility at the CPython level.It's hard to do much with a vague "we need to think bigger", but it seems likely that any type system support for templates will involve some variation of Template + square brackets, whether those square brackets hold the types of the interpolations or something else. Having this support in CPython now means that even if we specify a type system feature later, people will be able to use it even in CPython 3.14.
I'd encourage you to open a discussion in the typing section on Discuss to brainstorm what this bigger thing would look like. What would be helpful would be examples of how templates are expected to be used and what kinds of things you'd like type checkers to be able to help you catch.
sobolevn commentedon May 15, 2025
Probably any reference implementation for any of the type-checkers will also help a lot to decide what is best for the static typing.
lysnikolaou commentedon May 15, 2025
Okay, I misunderstood the PR then and this looks good.
I know that this is vague and I apologize for that. It's just that I can't really do better than that because I don't know what that "bigger thinking" might look like. There's ideas about combining type checking with static analysis for DSLs, but I don't think that's in scope for this thread.
The recommendation to open a thread on Discuss is good. I also did a lightning talk in the language summit to get people to think about this stuff. I'll try to do it ASAP, but I'm not able to lead that discussion.
erictraut commentedon Jun 11, 2025
Pyright 1.1.402 includes support for PEP 750. Here's an example.
One small complication with making
TemplateandInterpolationgeneric is that there is no way to explicitly specialize anInterpolation[T]by calling its constructor in the context of a t-string since its construction is implicitly called by the runtime. That means bidirectional type inference is the only option for influencing the type of the interpolations. I think this could work, but it's complicated by the concatenation rules for t-strings and regular strings (e.g.x: Template[int] = "foo" t"{bar}"). The inference context needs to span the concatenation.JelleZijlstra commentedon Jun 12, 2025
Note there's still an open discussion about changing the implicit concatenation behavior that the SC needs to decide on: python/steering-council#289
picnixz commentedon Jun 13, 2025
(adding triaged so that I don't remove the 3.14 label by mistake)
pythongh-133970: Make PEP750 types generic (python#133976)
pythongh-133970: Make PEP750 types generic (python#133976)