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 a str. They produce a string.templatelib.Template carrying two parallel sequences: the literal string pieces (strings) and the evaluated Interpolation objects (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

c
// 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 (a str, default "").
  • conversion – None or a one-character str ('s', 'r', 'a').
  • format_spec – the format specifier string (default "").

The type itself is a plain GC type:

c
// 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:

c
// 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:

c
// 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':

c
// 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:

c
// 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:

c
// 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:

c
// 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:

c
// 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

c
// 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:

c
// 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:

c
// 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:

c
// 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:

c
// 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:

c
// 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

c
// 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:

c
// Objects/templateobject.c
typedef struct {
    PyObject_HEAD
    PyObject *stringsiter;
    PyObject *interpolationsiter;
    int from_strings;
} templateiterobject;
c
// 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:

c
// 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:

c
// 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..]
}
c
// 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:

c
// 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:

c
// 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:

c
// 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:

c
// 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:

c
// 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;
}
c
// 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

  • Template and Interpolation are GC-tracked C types defined in Objects/templateobject.c and Objects/interpolationobject.c, exported as string.templatelib.Template and string.templatelib.Interpolation.
  • An Interpolation stores value, expression, conversion (None or a one-char str), and format_spec; conversion is validated by _conversion_converter in the public constructor and synthesized from FVC_* constants by _PyInterpolation_Build on the bytecode path.
  • A Template stores strings and interpolations tuples satisfying len(strings) == len(interpolations) + 1; template_new normalizes 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_INTERPOLATION and BUILD_TEMPLATE, which call _PyInterpolation_Build and _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_unpickle and the Python-level Template/Interpolation aliases.
  • Lib/test/test_string/test_templatelib.py – test coverage.

PythonCpythonInternalsStringsTemplate