CPython's Template String Implementation in C (t-strings): Interpolation as Data
Python 3.15's t-strings don't produce a str. They produce a Template carrying literal strings plus Interpolation objects, built in C by BUILD_TEMPLATE and BUILD_INTERPOLATION.
TL;DR: Python 3.14+ template strings (
t"...") don’t produce astr. They produce astring.templatelib.Templatecarrying two parallel sequences: the literal string pieces (strings) and the evaluatedInterpolationobjects (interpolations). Both types are C types, GC-tracked, built by two dedicated bytecodes.
Template holds the literal pieces and the interpolations side by side. Each Interpolation holds the value, the source expression text, an optional conversion flag, and an optional format spec. The implementation lives in Objects/templateobject.c and Objects/interpolationobject.c. Let’s trace it.
The Interpolation struct
// Objects/interpolationobject.c
typedef struct {
PyObject_HEAD
PyObject *value;
PyObject *expression;
PyObject *conversion;
PyObject *format_spec;
} interpolationobject;The four fields are all owned references:
value– the evaluated result of the interpolated expression.expression– the source text of the expression (astr, default"").conversion–Noneor a one-characterstr('s','r','a').format_spec– the format specifier string (default"").
The type itself is a plain GC type:
// Objects/interpolationobject.c
PyTypeObject _PyInterpolation_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "string.templatelib.Interpolation",
.tp_basicsize = sizeof(interpolationobject),
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
.tp_new = interpolation_new,
.tp_dealloc = interpolation_dealloc,
.tp_clear = interpolation_clear,
.tp_free = PyObject_GC_Del,
.tp_repr = interpolation_repr,
.tp_members = interpolation_members,
.tp_methods = interpolation_methods,
.tp_traverse = interpolation_traverse,
};tp_members exposes all four fields as read-only attributes, which is why interp.value, interp.expression, interp.conversion, and interp.format_spec work directly from Python:
// Objects/interpolationobject.c
static PyMemberDef interpolation_members[] = {
{"value", Py_T_OBJECT_EX, offsetof(interpolationobject, value), Py_READONLY, "Value"},
{"expression", Py_T_OBJECT_EX, offsetof(interpolationobject, expression), Py_READONLY, "Expression"},
{"conversion", Py_T_OBJECT_EX, offsetof(interpolationobject, conversion), Py_READONLY, "Conversion"},
{"format_spec", Py_T_OBJECT_EX, offsetof(interpolationobject, format_spec), Py_READONLY, "Format specifier"},
{NULL}
};Constructing an Interpolation
The public constructor is generated by Argument Clinic from:
// Objects/interpolationobject.c (clinic input)
@classmethod
Interpolation.__new__ as interpolation_new
value: object
expression: object(subclass_of='&PyUnicode_Type', c_default='&_Py_STR(empty)') = ""
conversion: object(converter='_conversion_converter') = None
format_spec: object(subclass_of='&PyUnicode_Type', c_default='&_Py_STR(empty)') = ""The converter _conversion_converter validates that conversion is either None or a single-character string among 's', 'r', 'a':
// Objects/interpolationobject.c
static int
_conversion_converter(PyObject *arg, PyObject **conversion)
{
if (arg == Py_None) {
return 1;
}
if (!PyUnicode_Check(arg)) {
...
return 0;
}
Py_ssize_t len;
const char *conv_str = PyUnicode_AsUTF8AndSize(arg, &len);
if (len != 1 || !(conv_str[0] == 'a' || conv_str[0] == 'r' || conv_str[0] == 's')) {
PyErr_SetString(PyExc_ValueError,
"Interpolation() argument 'conversion' must be one of 's', 'a' or 'r'");
return 0;
}
*conversion = arg;
return 1;
}The implementation allocates a GC object and takes new references to all four arguments:
// Objects/interpolationobject.c
static PyObject *
interpolation_new_impl(PyTypeObject *type, PyObject *value,
PyObject *expression, PyObject *conversion,
PyObject *format_spec)
{
interpolationobject *self = PyObject_GC_New(interpolationobject, type);
if (!self) {
return NULL;
}
self->value = Py_NewRef(value);
self->expression = Py_NewRef(expression);
self->conversion = Py_NewRef(conversion);
self->format_spec = Py_NewRef(format_spec);
PyObject_GC_Track(self);
return (PyObject *) self;
}There’s also an internal fast builder, _PyInterpolation_Build, used by the bytecode. It takes a C int conversion (one of FVC_NONE, FVC_STR, FVC_REPR, FVC_ASCII) rather than a Python object and maps it to the stored conversion value:
// Objects/interpolationobject.c
PyObject *
_PyInterpolation_Build(PyObject *value, PyObject *str, int conversion, PyObject *format_spec)
{
interpolationobject *interpolation = PyObject_GC_New(interpolationobject, &_PyInterpolation_Type);
...
interpolation->value = Py_NewRef(value);
interpolation->expression = Py_NewRef(str);
interpolation->format_spec = Py_NewRef(format_spec);
interpolation->conversion = NULL;
if (conversion == 0) { // FVC_NONE
interpolation->conversion = Py_None;
}
else {
switch (conversion) {
case FVC_ASCII: // 3
interpolation->conversion = _Py_LATIN1_CHR('a');
break;
case FVC_REPR: // 2
interpolation->conversion = _Py_LATIN1_CHR('r');
break;
case FVC_STR: // 1
interpolation->conversion = _Py_LATIN1_CHR('s');
break;
...
}
}
PyObject_GC_Track(interpolation);
return (PyObject *) interpolation;
}Note that FVC_NONE is 0, which is why conversion == 0 is tested directly.
This builder is the hot path: it avoids going through __new__ and the converter on every interpolated value. Instead of allocating a fresh one-character string for 'a'/'r'/'s', it reaches for the immortal Latin-1 singleton.
_PyInterpolation_InitTypes sets __match_args__ to the four field names so that structural pattern matching works:
// Objects/interpolationobject.c
PyStatus
_PyInterpolation_InitTypes(PyInterpreterState *interp)
{
PyObject *tuple = Py_BuildValue("(ssss)", "value", "expression", "conversion", "format_spec");
...
int status = PyDict_SetItemString(dict, "__match_args__", tuple);
...
}Pickling is supported through __reduce__, which returns the class and a 4-tuple of fields:
// Objects/interpolationobject.c
static PyObject*
interpolation_reduce(PyObject *op, PyObject *Py_UNUSED(dummy))
{
interpolationobject *self = interpolationobject_CAST(op);
return Py_BuildValue("(O(OOOO))", (PyObject *)Py_TYPE(op),
self->value, self->expression,
self->conversion, self->format_spec);
}The Template struct
// Objects/templateobject.c
typedef struct {
PyObject_HEAD
PyObject *strings;
PyObject *interpolations;
} templateobject;strings is a tuple of str whose length is len(interpolations) + 1; interpolations is a tuple of Interpolation. Both are owned references, and the object is GC-tracked:
// Objects/templateobject.c
PyTypeObject _PyTemplate_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "string.templatelib.Template",
.tp_basicsize = sizeof(templateobject),
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
.tp_as_sequence = &template_as_sequence,
.tp_new = template_new,
.tp_dealloc = template_dealloc,
.tp_clear = template_clear,
.tp_free = PyObject_GC_Del,
.tp_repr = template_repr,
.tp_members = template_members,
.tp_methods = template_methods,
.tp_getset = template_getset,
.tp_iter = template_iter,
.tp_traverse = template_traverse,
};The two tuple fields are exposed read-only:
// Objects/templateobject.c
static PyMemberDef template_members[] = {
{"strings", Py_T_OBJECT_EX, offsetof(templateobject, strings), Py_READONLY, "Strings"},
{"interpolations", Py_T_OBJECT_EX, offsetof(templateobject, interpolations), Py_READONLY, "Interpolations"},
{NULL},
};A computed values property returns a tuple of the interpolation values, mirroring Template.values in the Python-level API:
// Objects/templateobject.c
static PyObject *
template_values_get(PyObject *op, void *Py_UNUSED(data))
{
templateobject *self = templateobject_CAST(op);
Py_ssize_t len = PyTuple_GET_SIZE(self->interpolations);
PyObject *values = PyTuple_New(len);
if (values == NULL) {
return NULL;
}
for (Py_ssize_t i = 0; i < len; i++) {
PyObject *item = PyTuple_GET_ITEM(self->interpolations, i);
PyTuple_SET_ITEM(values, i, _PyInterpolation_GetValueRef(item));
}
return values;
}_PyInterpolation_GetValueRef simply returns a new reference to the interpolation’s value field.
Template.__new__: normalizing the argument list
The constructor accepts a flat sequence of str and Interpolation objects and normalizes them into the two parallel tuples. Consecutive strings are concatenated; an Interpolation with no preceding string gets an empty string inserted before it; a trailing interpolation gets an empty string appended:
// Objects/templateobject.c
static PyObject *
template_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
if (kwds != NULL) {
PyErr_SetString(PyExc_TypeError, "Template.__new__ only accepts *args arguments");
return NULL;
}
Py_ssize_t argslen = PyTuple_GET_SIZE(args);
Py_ssize_t stringslen = 0;
Py_ssize_t interpolationslen = 0;
int last_was_str = 0;
for (Py_ssize_t i = 0; i < argslen; i++) {
PyObject *item = PyTuple_GET_ITEM(args, i);
if (PyUnicode_Check(item)) {
if (!last_was_str) {
stringslen++;
}
last_was_str = 1;
}
else if (_PyInterpolation_CheckExact(item)) {
if (!last_was_str) {
stringslen++;
}
interpolationslen++;
last_was_str = 0;
}
else {
PyErr_Format(PyExc_TypeError,
"Template.__new__ *args need to be of type 'str' or 'Interpolation', got %T",
item);
return NULL;
}
}
if (!last_was_str) {
stringslen++;
}
...
}The second pass performs the concatenation and empty-string insertion, then delegates to _PyTemplate_Build:
// Objects/templateobject.c (second pass, abbreviated)
for (Py_ssize_t i = 0; i < argslen; i++) {
PyObject *item = PyTuple_GET_ITEM(args, i);
if (PyUnicode_Check(item)) {
if (last_was_str) {
PyObject *laststring = PyTuple_GET_ITEM(strings, stringsidx - 1);
PyObject *concat = PyUnicode_Concat(laststring, item);
...
PyTuple_SET_ITEM(strings, stringsidx - 1, concat);
Py_DECREF(laststring);
}
else {
PyTuple_SET_ITEM(strings, stringsidx++, Py_NewRef(item));
}
last_was_str = 1;
}
else if (_PyInterpolation_CheckExact(item)) {
if (!last_was_str) {
_Py_DECLARE_STR(empty, "");
PyTuple_SET_ITEM(strings, stringsidx++, &_Py_STR(empty));
}
PyTuple_SET_ITEM(interpolations, interpolationsidx++, Py_NewRef(item));
last_was_str = 0;
}
}
if (!last_was_str) {
_Py_DECLARE_STR(empty, "");
PyTuple_SET_ITEM(strings, stringsidx++, &_Py_STR(empty));
}
PyObject *template = _PyTemplate_Build(strings, interpolations);
Py_DECREF(strings);
Py_DECREF(interpolations);
return template;This normalization is what lets Template("a", interp, "b") and Template("a", interp, "b", interp2) both satisfy the invariant that len(strings) == len(interpolations) + 1.
_PyTemplate_Build: the factory
// Objects/templateobject.c
PyObject *
_PyTemplate_Build(PyObject *strings, PyObject *interpolations)
{
templateobject *template = PyObject_GC_New(templateobject, &_PyTemplate_Type);
if (template == NULL) {
return NULL;
}
template->strings = Py_NewRef(strings);
template->interpolations = Py_NewRef(interpolations);
PyObject_GC_Track(template);
return (PyObject *) template;
}Like _PyInterpolation_Build, this is the internal fast path used by the bytecode and by template_new itself.
Iteration: TemplateIter
Template.__iter__ returns a GC-tracked templateiterobject that alternates between the strings and interpolations iterators, skipping empty strings that result from adjacent interpolations:
// Objects/templateobject.c
typedef struct {
PyObject_HEAD
PyObject *stringsiter;
PyObject *interpolationsiter;
int from_strings;
} templateiterobject;// Objects/templateobject.c
static PyObject *
templateiter_next(PyObject *op)
{
templateiterobject *self = templateiterobject_CAST(op);
PyObject *item;
if (self->from_strings) {
item = PyIter_Next(self->stringsiter);
self->from_strings = 0;
if (item == NULL) {
return NULL;
}
if (PyUnicode_GET_LENGTH(item) == 0) {
Py_SETREF(item, PyIter_Next(self->interpolationsiter));
self->from_strings = 1;
}
}
else {
item = PyIter_Next(self->interpolationsiter);
self->from_strings = 1;
}
return item;
}The from_strings flag starts at 1, so iteration begins with a string. When that string is empty (which happens between two interpolations), it’s replaced by the next interpolation and the flag is reset so the following string is consumed next. That yields the familiar str/Interpolation alternation you get from for part in template:.
template_iter constructs the iterator and obtains iterators over both tuples:
// Objects/templateobject.c
static PyObject *
template_iter(PyObject *op)
{
templateobject *self = templateobject_CAST(op);
templateiterobject *iter = PyObject_GC_New(templateiterobject, &_PyTemplateIter_Type);
...
PyObject *stringsiter = PyObject_GetIter(self->strings);
...
PyObject *interpolationsiter = PyObject_GetIter(self->interpolations);
...
iter->stringsiter = stringsiter;
iter->interpolationsiter = interpolationsiter;
iter->from_strings = 1;
PyObject_GC_Track(iter);
return (PyObject *)iter;
}Concatenation
Template supports + via tp_as_sequence.sq_concat. When both operands are exact Template instances, their strings and interpolations are concatenated, merging the boundary strings:
// Objects/templateobject.c
static PyObject *
template_strings_concat(PyObject *left, PyObject *right)
{
Py_ssize_t left_stringslen = PyTuple_GET_SIZE(left);
PyObject *left_laststring = PyTuple_GET_ITEM(left, left_stringslen - 1);
Py_ssize_t right_stringslen = PyTuple_GET_SIZE(right);
PyObject *right_firststring = PyTuple_GET_ITEM(right, 0);
PyObject *concat = PyUnicode_Concat(left_laststring, right_firststring);
...
PyObject *newstrings = PyTuple_New(left_stringslen + right_stringslen - 1);
...
// copy left[0..n-2], then concat, then right[1..]
}// Objects/templateobject.c
static PyObject *
template_concat_templates(templateobject *self, templateobject *other)
{
PyObject *newstrings = template_strings_concat(self->strings, other->strings);
...
PyObject *newinterpolations = PySequence_Concat(self->interpolations, other->interpolations);
...
PyObject *newtemplate = _PyTemplate_Build(newstrings, newinterpolations);
...
}_PyTemplate_Concat is the public entry point and rejects non-Template operands with a TypeError.
Pickling
Template.__reduce__ delegates to string.templatelib._template_unpickle, passing the two tuples:
// Objects/templateobject.c
static PyObject*
template_reduce(PyObject *op, PyObject *Py_UNUSED(dummy))
{
PyObject *mod = PyImport_ImportModule("string.templatelib");
...
PyObject *func = PyObject_GetAttrString(mod, "_template_unpickle");
...
templateobject *self = templateobject_CAST(op);
PyObject *result = Py_BuildValue("O(OO)", func, self->strings, self->interpolations);
...
}The Python helper _template_unpickle in Lib/string/templatelib.py zips the strings and interpolations back into the flat str/Interpolation argument list and calls Template(*parts), re-running the normalization in template_new.
From source to object: the compiler and bytecode
The parser produces a TemplateStr expression node whose values sequence contains Constant (string) and Interpolation nodes. The compiler’s codegen_template (in Python/codegen.c) emits the strings as a tuple, the interpolations as a tuple, and then BUILD_TEMPLATE:
// Python/codegen.c
Py_ssize_t stringslen = 0;
for (Py_ssize_t i = 0; i < value_count; i++) {
value = asdl_seq_GET(e->v.TemplateStr.values, i);
if (value->kind == Interpolation_kind) {
if (last_was_interpolation) {
ADDOP_LOAD_CONST(c, loc, Py_NewRef(&_Py_STR(empty)));
stringslen++;
}
last_was_interpolation = 1;
}
else {
VISIT(c, expr, value);
stringslen++;
last_was_interpolation = 0;
}
}
if (last_was_interpolation) {
ADDOP_LOAD_CONST(c, loc, Py_NewRef(&_Py_STR(empty)));
stringslen++;
}
ADDOP_I(c, loc, BUILD_TUPLE, stringslen);
Py_ssize_t interpolationslen = 0;
for (Py_ssize_t i = 0; i < value_count; i++) {
value = asdl_seq_GET(e->v.TemplateStr.values, i);
if (value->kind == Interpolation_kind) {
VISIT(c, expr, value);
interpolationslen++;
}
}
ADDOP_I(c, loc, BUILD_TUPLE, interpolationslen);
ADDOP(c, loc, BUILD_TEMPLATE);Each Interpolation node is compiled by codegen_interpolation, which visits the value, loads the expression string constant, optionally visits the format spec, encodes the conversion into the oparg, and emits BUILD_INTERPOLATION:
// Python/codegen.c
VISIT(c, expr, e->v.Interpolation.value);
ADDOP_LOAD_CONST(c, loc, e->v.Interpolation.str);
int oparg = 2;
if (e->v.Interpolation.format_spec) {
oparg++;
VISIT(c, expr, e->v.Interpolation.format_spec);
}
int conversion = e->v.Interpolation.conversion;
if (conversion != -1) {
switch (conversion) {
case 's': oparg |= FVC_STR << 2; break;
case 'r': oparg |= FVC_REPR << 2; break;
case 'a': oparg |= FVC_ASCII << 2; break;
...
}
}
ADDOP_I(c, loc, BUILD_INTERPOLATION, oparg);The bytecode implementation consumes the stack and calls the C builders:
// Python/bytecodes.c
inst(BUILD_INTERPOLATION, (value, str, format[oparg & 1] -- interpolation)) {
PyObject *value_o = PyStackRef_AsPyObjectBorrow(value);
PyObject *str_o = PyStackRef_AsPyObjectBorrow(str);
int conversion = oparg >> 2;
PyObject *format_o;
if (oparg & 1) {
format_o = PyStackRef_AsPyObjectBorrow(format[0]);
}
else {
format_o = &_Py_STR(empty);
}
PyObject *interpolation_o = _PyInterpolation_Build(value_o, str_o, conversion, format_o);
...
interpolation = PyStackRef_FromPyObjectSteal(interpolation_o);
}
inst(BUILD_TEMPLATE, (strings, interpolations -- template)) {
PyObject *strings_o = PyStackRef_AsPyObjectBorrow(strings);
PyObject *interpolations_o = PyStackRef_AsPyObjectBorrow(interpolations);
PyObject *template_o = _PyTemplate_Build(strings_o, interpolations_o);
PyStackRef_CLOSE(interpolations);
PyStackRef_CLOSE(strings);
ERROR_IF(template_o == NULL);
template = PyStackRef_FromPyObjectSteal(template_o);
}The oparg & 1 bit of BUILD_INTERPOLATION indicates whether a format spec was supplied; the conversion is carried in the upper bits (oparg >> 2).
Memory layout and GC
Both types are GC objects with tp_traverse/tp_clear visiting every owned reference:
// Objects/interpolationobject.c
static int
interpolation_traverse(PyObject *op, visitproc visit, void *arg)
{
interpolationobject *self = interpolationobject_CAST(op);
Py_VISIT(self->value);
Py_VISIT(self->expression);
Py_VISIT(self->conversion);
Py_VISIT(self->format_spec);
return 0;
}// Objects/templateobject.c
static int
template_traverse(PyObject *op, visitproc visit, void *arg)
{
templateobject *self = templateobject_CAST(op);
Py_VISIT(self->strings);
Py_VISIT(self->interpolations);
return 0;
}The iterator is also GC-tracked because it holds two iterators. All three use PyObject_GC_Del as tp_free and untrack themselves in tp_dealloc before clearing.
The on-disk sizes are small: an Interpolation is a PyObject header plus four pointers, and a Template is a header plus two pointers. Both are allocated through PyObject_GC_New, so they participate in cyclic garbage collection like tuples and lists.
Key takeaways
TemplateandInterpolationare GC-tracked C types defined inObjects/templateobject.candObjects/interpolationobject.c, exported asstring.templatelib.Templateandstring.templatelib.Interpolation.- An
Interpolationstoresvalue,expression,conversion(Noneor a one-charstr), andformat_spec;conversionis validated by_conversion_converterin the public constructor and synthesized fromFVC_*constants by_PyInterpolation_Buildon the bytecode path. - A
Templatestoresstringsandinterpolationstuples satisfyinglen(strings) == len(interpolations) + 1;template_newnormalizes a flat argument list into that invariant. - Iteration alternates strings and interpolations, skipping empty boundary strings; concatenation merges the boundary strings.
- The compiler emits
BUILD_INTERPOLATIONandBUILD_TEMPLATE, which call_PyInterpolation_Buildand_PyTemplate_Build. - Both objects are GC-tracked with full traverse/clear support and support pickling via
__reduce__.
Further reading
Objects/templateobject.c–_PyTemplate_Type,_PyTemplateIter_Type,template_new,_PyTemplate_Build,template_iter,template_values_get,_PyTemplate_Concat,template_reduce.Objects/interpolationobject.c–_PyInterpolation_Type,interpolation_new_impl,_PyInterpolation_Build,_PyInterpolation_InitTypes,interpolation_reduce.Include/internal/pycore_template.h,Include/internal/pycore_interpolation.h– type checks and builder declarations.Python/codegen.c–codegen_template,codegen_interpolation.Python/bytecodes.c–BUILD_INTERPOLATION,BUILD_TEMPLATE.Lib/string/templatelib.py–_template_unpickleand the Python-levelTemplate/Interpolationaliases.Lib/test/test_string/test_templatelib.py– test coverage.