T-String Templates
Requires Python 3.14+
Template Strings let you write templates as Python code using the new template string literals from PEP 750. This gives you full IDE support, type checking, and the ability to compose templates using regular Python functions.
Note: For Python 3.11-3.13, see HTML Templates instead.
Basic Usage
Instead of creating .html files, extend TemplateView and define a template() method on your LiveView:
from pyview import LiveView, LiveViewSocketfrom pyview.template import TemplateViewfrom string.templatelib import Templatefrom pyview.meta import PyViewMetafrom pyview.events import AutoEventDispatch, eventfrom typing import TypedDict
class CountContext(TypedDict): count: int
class CounterLiveView(AutoEventDispatch, TemplateView, LiveView[CountContext]): async def mount(self, socket: LiveViewSocket[CountContext], session): socket.context = CountContext({"count": 0})
@event async def increment(self, event, payload, socket): socket.context["count"] += 1
@event async def decrement(self, event, payload, socket): socket.context["count"] -= 1
def button(self, action, text: str) -> Template: return t"""<button phx-click={action}>{text}</button>"""
def template(self, assigns: CountContext, meta: PyViewMeta) -> Template: count = assigns["count"] return t"""<div> <h1>Count: {count}</h1> {self.button(self.increment, "+")} {self.button(self.decrement, "-")} </div>"""Key points:
- Inherit from both
TemplateViewandLiveView - Implement
template(self, assigns, meta)returning aTemplate - Use
t"..."ort"""..."""for template literals - Use
{variable}for interpolation (not{{variable}}like Ibis)
Variable Interpolation
Variables from your context are interpolated using {variable} syntax:
def template(self, assigns: CountContext, meta: PyViewMeta) -> Template: name = assigns["name"] email = assigns["email"]
return t"""<div class="user-card"> <h2>{name}</h2> <p>{email}</p> </div>"""Automatic HTML Escaping
All string interpolations are automatically HTML-escaped for security:
# If name contains "<script>alert('xss')</script>"# Output will be: "<p>Hello, <script>alert('xss')</script></p>"return t"<p>Hello, {name}</p>"To insert raw HTML, use an object with an __html__() method:
from markupsafe import Markup
def template(self, assigns, meta): raw_html = Markup("<strong>Bold text</strong>") return t"<div>{raw_html}</div>"Composable Helper Methods
One of the biggest advantages of t-strings is composability. You can create helper methods that return template fragments:
class CounterLiveView(TemplateView, LiveView[CountContext]):
def button(self, label: str, event: str, style: str = "primary") -> Template: """Reusable button component.""" if style == "primary": classes = "bg-blue-600 text-white hover:bg-blue-700" else: classes = "bg-gray-200 text-gray-800 hover:bg-gray-300"
return t"""<button phx-click="{event}" class="{classes}"> {label} </button>"""
def template(self, assigns: CountContext, meta: PyViewMeta) -> Template: count = assigns["count"] return t"""<div> <h1>{count}</h1> <div class="buttons"> {self.button("−", "decrement", "secondary")} {self.button("+", "increment", "primary")} </div> </div>"""Helper methods can be reused across your view and even extracted to mixins for cross-view reuse.
Using with AutoEventDispatch
T-strings work especially well with AutoEventDispatch, which lets you reference event handler methods directly in templates:
from pyview import LiveView, LiveViewSocketfrom pyview.events import AutoEventDispatch, eventfrom pyview.template import TemplateView
class CounterLiveView(AutoEventDispatch, TemplateView, LiveView[CountContext]): async def mount(self, socket, session): socket.context = CountContext({"count": 0})
@event async def decrement(self, event, payload, socket): socket.context["count"] -= 1
@event async def increment(self, event, payload, socket): socket.context["count"] += 1
def button(self, label: str, event_ref, style: str = "primary") -> Template: classes = "px-4 py-2 rounded" return t"""<button phx-click="{event_ref}" class="{classes}">{label}</button>"""
def template(self, assigns: CountContext, meta: PyViewMeta) -> Template: count = assigns["count"] return t"""<div> <h1>{count}</h1> {self.button("−", self.decrement)} {self.button("+", self.increment)} </div>"""Notice phx-click="{self.decrement}" - the method reference automatically converts to the event name string. This eliminates the duplication of event name strings throughout your code.
See Event Handling - AutoEventDispatch for more details.
Lists and Loops
For rendering lists, use list comprehensions that produce template fragments:
def template(self, assigns, meta) -> Template: users = assigns["users"]
# List comprehension producing Template objects user_rows = [ t"""<tr> <td>{user["name"]}</td> <td>{user["email"]}</td> </tr>""" for user in users ]
return t"""<table> <thead> <tr><th>Name</th><th>Email</th></tr> </thead> <tbody> {user_rows} </tbody> </table>"""You can also use helper methods for more complex list items:
def user_row(self, user: dict) -> Template: return t"""<tr> <td>{user["name"]}</td> <td>{user["email"]}</td> <td> <button phx-click="edit" phx-value-id="{user["id"]}">Edit</button> </td> </tr>"""
def template(self, assigns, meta) -> Template: users = assigns["users"] return t"""<table> <tbody> {[self.user_row(u) for u in users]} </tbody> </table>"""Migration from HTML Templates
To migrate an existing view from HTML templates to t-strings:
-
Add
TemplateViewto your class inheritance:# Beforeclass MyView(LiveView[MyContext]):# Afterclass MyView(TemplateView, LiveView[MyContext]): -
Move your HTML to a
template()method, converting syntax:{{variable}}→{variable}{% if condition %}...{% endif %}→ Python conditionals{% for item in items %}...{% endfor %}→ List comprehensions{{ value | filter }}→ Python function calls
-
Delete the
.htmlfile (PyView will usetemplate()instead) -
Optionally add
AutoEventDispatchfor cleaner event binding
Best Practices
Extract Reusable Components
# Create helper methods for repeated patternsdef card(self, title: str, content: Template) -> Template: return t"""<div class="card"> <h3 class="card-title">{title}</h3> <div class="card-body">{content}</div> </div>"""
def template(self, assigns, meta) -> Template: return t"""<div> {self.card("Users", self.user_list(assigns["users"]))} {self.card("Stats", self.stats_panel(assigns["stats"]))} </div>"""