Extensions let you add custom template tags to Kida without modifying core code. An extension registers new block keywords, parses them into AST nodes, and compiles those nodes into Python statements.
Extension Base Class
Subclasskida.extensions.Extensionand declare your tag keywords, node types, and optional end keywords:
from kida.extensions import Extension
class MyExtension(Extension):
tags = {"mytag"} # Block keywords this extension handles
node_types = {"MyTagNode"} # Node class names this extension compiles
end_keywords = {"endmytag"} # End keywords for tags with bodies (optional)
Every extension receives the Environmentinstance on initialization and can provide filters, tests, and globals alongside custom tags:
class MyExtension(Extension):
tags = {"mytag"}
node_types = {"MyTagNode"}
def get_filters(self):
return {"myfilter": lambda v: v.upper()}
def get_tests(self):
return {"even": lambda v: v % 2 == 0}
def get_globals(self):
return {"MY_CONST": 42}
Registering Extensions
Pass extension classes (not instances) to theEnvironment:
from kida import Environment
env = Environment(extensions=[MyExtension])
On initialization, the environment:
- Instantiates each extension class with itself (
ext = ExtClass(self)) - Registers
get_filters(),get_tests(), andget_globals()results - Maps each tag in
tagsto the extension instance (parser dispatch) - Maps each name in
node_typesto the extension instance (compiler dispatch) - Merges
end_keywordsinto the parser's end-keyword set
Custom Node Types
Define your AST node as a frozen dataclass inheriting fromNode:
from dataclasses import dataclass
from kida.nodes.base import Node
@dataclass(frozen=True, slots=True)
class MyTagNode(Node):
name: str
body: tuple # child nodes
The Node base class provides lineno and col_offset fields for source mapping. Use frozen=True and slots=Truefor immutable, memory-efficient nodes.
Compiler Dispatch
The compiler resolves nodes by type name. When it encounters a node whose class name matches anode_types entry, it calls that extension's compile()method:
Compiler._compile_node(node)
-> look up type(node).__name__ in _extension_compilers
-> ext.compile(compiler, node)
This is an O(1) dictionary lookup. The extension'scompile() method must return a list[ast.stmt]of Python AST statements.
End Keywords
If your custom tag wraps a body (like{% mytag %}...{% endmytag %}), declare the closing keyword in end_keywordsso the parser recognizes it:
class MyExtension(Extension):
tags = {"mytag"}
node_types = {"MyTagNode"}
end_keywords = {"endmytag"}
End keywords are merged into the parser's keyword set at initialization. Without this, the parser will not recognize {% endmytag %}as a valid block terminator.
You can also use the universal{% end %} syntax to close extension tags, but declaring end_keywordsenables the explicit form.
Example
Build a complete{% debug expr %}tag that prints a value during rendering:
import ast
from dataclasses import dataclass
from kida import Environment
from kida.extensions import Extension
from kida.nodes.base import Node
@dataclass(frozen=True, slots=True)
class DebugNode(Node):
expr: object # Expr node from parser
class DebugExtension(Extension):
tags = {"debug"}
node_types = {"DebugNode"}
def parse(self, parser, tag_name):
# Current token is "debug" — advance past it
token = parser._advance()
# Parse the expression after the tag name
expr = parser._parse_expression()
# Expect the block-end delimiter (%})
parser._expect(parser.TokenType.BLOCK_END)
return DebugNode(
lineno=token.lineno,
col_offset=token.col_offset,
expr=expr,
)
def compile(self, compiler, node):
# Compile the Kida expression into a Python AST expression
expr = compiler._compile_expr(node.expr)
# Return a print() call statement
return [
ast.Expr(
value=ast.Call(
func=ast.Name(id="print", ctx=ast.Load()),
args=[expr],
keywords=[],
)
)
]
# Register and use
env = Environment(extensions=[DebugExtension])
Template usage:
{% debug user.name %}
{# Prints the value of user.name to stdout during rendering #}
Extension with a Body
For tags that wrap content, declareend_keywords and use _parse_body():
@dataclass(frozen=True, slots=True)
class TimedNode(Node):
label: object
body: tuple
class TimedExtension(Extension):
tags = {"timed"}
node_types = {"TimedNode"}
end_keywords = {"endtimed"}
def parse(self, parser, tag_name):
token = parser._advance()
label = parser._parse_expression()
parser._expect(parser.TokenType.BLOCK_END)
body = parser._parse_body()
# Consume {% end %} or {% endtimed %}
parser._advance() # {%
parser._advance() # end/endtimed
parser._expect(parser.TokenType.BLOCK_END)
return TimedNode(
lineno=token.lineno,
col_offset=token.col_offset,
label=label,
body=tuple(body),
)
def compile(self, compiler, node):
# Compile body nodes
body_stmts = []
for child in node.body:
body_stmts.extend(compiler._compile_node(child))
label_expr = compiler._compile_expr(node.label)
# Wrap body in timing instrumentation
return [
ast.Expr(value=ast.Call(
func=ast.Name(id="print", ctx=ast.Load()),
args=[ast.JoinedStr(values=[
ast.Constant(value="[timed] start: "),
ast.FormattedValue(value=label_expr, conversion=-1),
])],
keywords=[],
)),
*body_stmts,
ast.Expr(value=ast.Call(
func=ast.Name(id="print", ctx=ast.Load()),
args=[ast.JoinedStr(values=[
ast.Constant(value="[timed] end: "),
ast.FormattedValue(value=label_expr, conversion=-1),
])],
keywords=[],
)),
]
Template usage:
{% timed "sidebar" %}
{% include "partials/sidebar.html" %}
{% end %}
See Also
- Custom Filters -- Add custom filters
- Custom Tests -- Add custom tests
- Custom Globals -- Add global variables
- Custom Loaders -- Add template loaders