mrkeyoor.com_
Thu 06 Aug 02:43 UTC
PyPIUtilsupdated 05 Aug 2026

Mako

Mako is a Python templating language that compiles each template into a real Python module and then runs it. Text passes through, ${expr} substitutes a Python expression, lines beginning with % are Python control statements (% for, % if, % endfor), and <% %> blocks hold ordinary Python code. Because templates become Python, you can use imports, comprehensions, and any expression you like, with no restricted subset to learn. It adds the pieces you need for real pages: reusable functions with <%def>, template inheritance with <%inherit> and <%block>, cross-template imports with <%namespace>, and a TemplateLookup that finds templates on disk and caches the compiled modules. It is maintained by the SQLAlchemy project.

Verdict

A fast, honest template engine for people who would rather write Python than learn a template dialect, and the right choice when you are already inside Alembic or Pyramid. For a new web app, Jinja2's escape-by-default behavior and much larger ecosystem win unless you have a specific reason to want unrestricted Python in your templates.

API stability5/5Template, TemplateLookup, and the template syntax have been unchanged for over a decade; 1.4.0 raised the Python floor to 3.10 and MarkupSafe to 2.0 without touching any API.
Docs4/5docs.makotemplates.org covers syntax, inheritance, namespaces, filters, caching, and the runtime API thoroughly with examples, in the same careful style as the SQLAlchemy docs; it reads as reference material rather than a tutorial, and the README is a single page.
Maintenance4/51.4.0 and 1.4.1 both shipped in August 2026 with the repository pushed the same week and a small tracker (56 open issues, 2 open PRs), but it is one maintainer working across the SQLAlchemy projects.
Ecosystem3/5Roughly 47M downloads a week, almost all of it Alembic listing Mako as a hard dependency; direct users are mostly Pyramid and Pylons codebases, and tooling support is thin next to Jinja2.

Use it if

  • You want full Python inside templates without learning a sandboxed dialect: comprehensions, imports at the top of the file, and arbitrary expressions all work as written
  • Rendering speed matters and templates are stable: they compile to .py modules that are cached in module_directory, so after the first run there is no parsing, only a function call
  • You are customizing an Alembic migration template (script.py.mako) or maintaining a Pyramid or Pylons application, where Mako is already the templating layer
  • You generate code, config, SQL, or emails rather than user-facing HTML, so autoescaping is not the property you need and raw Python power is
Skip it if

Setup reality

pip install mako brings in MarkupSafe 2.0 or later and nothing else. The setup work is deciding on three settings up front, because retrofitting them hurts. module_directory turns compiled templates into cached .py files on disk, which is the difference between fast and slow in production, but that directory must be writable and should be wiped on deploy or you can serve stale compiled templates. default_filters=['h'] is how you get HTML escaping, and adding it to an existing codebase double-escapes everywhere you already wrote | h by hand. strict_undefined=True turns a missing variable into a NameError instead of silently rendering nothing, which is what you want and also what will surface bugs in every template you already have. Errors are the other rough edge: a traceback points into the generated module by default, so wire up mako.exceptions.text_error_template early or you will spend time reading line numbers that do not exist in your source.

Patterns

Render a template from a stringrender-a-string-template

from mako.template import Template

tmpl = Template("hello ${name}, you have ${count} messages")
print(tmpl.render(name="world", count=3))

render() takes keyword arguments and returns a str. Any name used in the template that you do not pass renders as an empty string by default rather than raising, which is why strict_undefined exists.

Look up templates in a directory and cache the compiled modulesload-templates-from-disk

from mako.lookup import TemplateLookup

lookup = TemplateLookup(
    directories=['templates'],
    module_directory='/tmp/mako_modules',
    default_filters=['h'],
    strict_undefined=True,
    filesystem_checks=False,
)

tmpl = lookup.get_template('/pages/index.html')
print(tmpl.render(user=current_user))

module_directory caches the generated .py next to a hash of the path, so restarts are fast; clear it on deploy. filesystem_checks=False stops Mako from stat-ing the source on every render, which is right in production and wrong in development.

Loops and conditionals with control linescontrol-structures

% if users:
  <ul>
  % for i, user in enumerate(users):
    <li class="${'odd' if i % 2 else 'even'}">${user.name}</li>
  % endfor
  </ul>
% else:
  <p>Nobody here.</p>
% endif

Every block needs an explicit % endfor or % endif because whitespace is not significant in templates. A literal percent sign at the start of a line must be written %%.

Escape HTML and apply filters to an expressionescape-output

${comment.body | h}
${page.title | trim, h}
${next_url | u}
${raw_html | n}

<%!
    def shout(text):
        return text.upper() + "!"
%>
${greeting | shout}

Filters chain left to right. h is HTML escaping, u is URL escaping, trim strips whitespace, and n disables the default filters for that one expression, which is how you emit trusted markup when default_filters=['h'] is on.

Define and call reusable template functionsreusable-defs

<%def name="row(item, cls='')">
  <tr class="${cls}">
    <td>${item.name}</td>
    <td>${item.price}</td>
  </tr>
</%def>

<table>
% for item in items:
  ${row(item, cls='striped')}
% endfor
</table>

A def is compiled into a real Python function with real default arguments. Calling it with ${...} writes its output; defs can also be called from other templates through a namespace.

Share a layout with inherit and blockstemplate-inheritance

## base.html
<html>
  <head><title><%block name="title">Site</%block></title></head>
  <body>
    <div id="content">${self.body()}</div>
  </body>
</html>

## index.html
<%inherit file="base.html"/>
<%block name="title">Home</%block>
<p>Welcome.</p>

Control starts in the parent, not the child: base.html runs and calls self.body() where the child's content belongs. Use next.body() instead of self.body() when the chain is more than two levels deep and you want the immediate child.

Pull defs in from another file with a namespaceimport-from-another-template

<%namespace name="widgets" file="/lib/widgets.html"/>
<%namespace file="/lib/forms.html" import="input_field, submit"/>

${widgets.card(title='Sales', body=report)}
${input_field('email', value=user.email)}

Without import=, defs stay under the namespace name; with it they land in the local namespace. A file namespace needs a TemplateLookup, so this fails on a bare Template built from a string.

Run Python at module level versus render timepython-code-blocks

<%!
    # module level: runs once when the template is compiled
    import json
    from datetime import datetime
%>
<%
    # render level: runs on every render, sees the context
    total = sum(line.amount for line in invoice.lines)
    generated = datetime.utcnow().isoformat()
%>
<p>Total: ${total}</p>
<script>var data = ${json.dumps(chart) | n};</script>

The two blocks are not interchangeable. Imports and helper functions belong in <%! %> so they are not re-executed per render; anything touching request data must be in <% %> because module-level code cannot see the context.

Fail loudly on a missing variablecatch-undefined-variables

from mako.template import Template

tmpl = Template("Hello ${nmae}", strict_undefined=True)
tmpl.render(name="Ada")
# NameError: 'nmae' is not defined

# without strict_undefined this renders "Hello " and says nothing

The default UNDEFINED renders as an empty string, so a typo in a variable name silently produces a blank page section. Turn strict_undefined on in new projects; in old ones expect it to find real bugs on first run.

Get a traceback that points at the templatereadable-error-pages

from mako import exceptions
from mako.template import Template

try:
    print(Template("${1/0}").render())
except Exception:
    print(exceptions.text_error_template().render())
    # or exceptions.html_error_template().render() for a browser page

Raw tracebacks point into the generated module, not your template file. These helpers rewrite the frames back to template line numbers, and html_error_template highlights the source when Pygments is installed.

Write output straight to a file or responsestream-into-a-buffer

from mako.runtime import Context
from mako.template import Template

tmpl = Template(filename='templates/report.txt', module_directory='/tmp/mako')

with open('out.txt', 'w') as fh:
    tmpl.render_context(Context(fh, rows=rows, title='Q3'))

render() builds the whole string in memory; render_context writes through a buffer as it goes, which matters for large reports and for streaming HTTP responses. The Context takes the writable object first, then the template variables.

Customize the Alembic migration templatecustomize-alembic-template

## alembic/script.py.mako
"""${message}

Revision ID: ${up_revision}
Created: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}


def upgrade() -> None:
    ${upgrades if upgrades else "pass"}


def downgrade() -> None:
    raise NotImplementedError("forward-only migrations")

This file is the only Mako most people ever edit, and it is why Mako is in your lockfile. Alembic passes a fixed set of variables into it, so adding a house rule such as a forbidden downgrade or a ticket header is a one-line template change.

Alternatives

PackageRegistryPick it when
Jinja2PyPIYou are rendering HTML for a web app and want autoescaping on by default, a sandbox mode, and the templating language every Python tool already speaks.
ChameleonPyPIYou want fast attribute-based templates that stay valid HTML documents and can be opened in a browser or a designer's editor.
GenshiPyPIYou need XML-aware streaming templates where the output must be well-formed markup by construction.