Mako review
Mako 1.4.1 compiles text templates into Python modules and executes them. `${...}` evaluates expressions, percent lines handle control flow, and defs, blocks, namespaces, and inheritance split larger templates. Template authors can import modules and run ordinary Python, which makes Mako useful for code generation and makes hostile templates unsafe. Release 1.4.1 repairs a 1.4.0 packaging mistake that installed the repository's `tools` directory as a top-level package and could shadow an application's own `tools`. Our Python 3.12 install imported in 0.01 seconds, but it included no `py.typed` marker.
Mako 1.4.1 installed in 0.2 seconds, used 1 MB across 2 packages, and imported in 0.01 seconds in our sandbox. It fits Alembic customization and trusted Python-heavy generation; do not install it for user-authored templates or HTML that must be safe without explicit escaping.
We installed it
| Install | ✓ · 0.2s | 2 packages on disk · 1 MB |
| Import | ✓ | import mako in 0.01s · pure Python · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does Mako install cleanly?
Yes. In a fresh container with an empty cache, pip install Mako finished in 0.2s, leaving 2 packages and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does Mako need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import mako succeeded in 0.01s.
Mako or Jinja2: which should you use?
Jinja2: Use it for web templates that should escape HTML by default and expose a more restricted expression language. Mako 1.4.1 installed in 0.2 seconds, used 1 MB across 2 packages, and imported in 0.01 seconds in our sandbox.
When should you not use Mako?
Users or other untrusted parties can edit templates. Mako executes Python and does not claim to sandbox hostile template source.
Use it if
- Templates generate code, configuration, migration scripts, email, or markup and need direct access to Python expressions.
- An Alembic setup already renders `script.py.mako` and generated revisions need project-specific imports or structure.
- An existing Pyramid, Pylons, or Mako application relies on callable defs, namespaces, and inheritance.
- Repeated rendering benefits from compiled Python modules cached in a deployment-owned directory.
- Users or other untrusted parties can edit templates. Mako executes Python and does not claim to sandbox hostile template source.
- HTML output must escape by default with no setup. Mako's default filter is `str`; configure `h` or apply it to each expression.
- Request data directly selects a template URI. Releases 1.3.11 and 1.3.12 fixed separate `TemplateLookup` traversal paths involving double slashes and Windows backslashes.
- The application still runs Python 3.9 or older. Mako 1.4.1 requires Python 3.10 or newer.
- The team wants a deliberately restricted template language with broader web-framework examples and sandbox-oriented controls; Jinja2 is usually the closer fit.
Setup reality
Our install of Mako 1.4.1 finished in 0.2 seconds in a fresh Python 3.12 container. Two packages occupied 1 MB, import mako took 0.01 seconds, and pip-audit reported 0 known vulnerabilities. PyPI lists 4 direct requirements when extras are counted and requires Python 3.10 or newer. The package is pure Python and has no py.typed marker. Its installed metadata did not state a license, although the repository README describes an MIT-style license.
No service, credentials, native compiler, or project config file is needed. The important choices are lookup directories, HTML escaping, missing-name behavior, and compilation storage. Never pass unchecked request text to TemplateLookup.get_template(). For HTML, set default_filters=['h'] or escape each expression; the n filter disables defaults and belongs only on trusted markup. strict_undefined=True turns a missing value into a useful named error.
module_directory writes generated Python so another process can reuse compiled templates. That path needs application write permission, and deployment must remove stale modules when source changes. filesystem_checks controls source modification checks before a cached template is reused. Keep it enabled while editing. An immutable production image can disable it, but only if each rollout refreshes the cache. Version 1.4.1 also avoids the top-level tools package accidentally shipped in 1.4.0.
Template exceptions originate in generated modules. text_error_template() and html_error_template() translate their frames back to template source; never expose the HTML diagnostic to users because it can reveal source and render data. Mako 1.4 improves warnings and source lines for string templates, but the changelog notes that compilation temporarily replaces the process-wide warnings.showwarning. Precompiling trusted templates during startup reduces that cross-thread window.
Patterns
Render a trusted in-memory template render-template-string
from mako.template import Template
template = Template("Hello ${name}. Total: ${sum(values)}")
output = template.render(name="Ada", values=[4, 7, 9])`${...}` executes Python in Mako 1.4.1. The source string must come from the application, never from a user-editable field.
Load templates with escaping and a module cache configure-template-lookup
from mako.lookup import TemplateLookup
lookup = TemplateLookup(
directories=["templates"],
module_directory="/var/cache/myapp/mako",
default_filters=["h"],
strict_undefined=True,
filesystem_checks=False,
)
page = lookup.get_template("/pages/home.html")The module directory must be writable. Disable filesystem checks only for immutable source, and never build the template URI from unchecked request data.
Loop and branch with percent lines write-control-flow
% if orders:
<ul>
% for order in orders:
<li>${order.number}: ${order.total}</li>
% endfor
</ul>
% else:
<p>No orders found.</p>
% endifEvery Mako control block has an explicit `% end...` line. Write `%%` when a literal percent must appear at the beginning of an output line.
Choose escaping for each output value escape-html-values
<p>${comment.author | h}</p>
<div>${comment.body | h}</div>
<a href="${next_url | u}">Next</a>
<div>${trusted_fragment | n}</div>`h` escapes HTML, `u` encodes a URL value, and `n` cancels default filters. Mako 1.4.1 does not escape with `h` unless you configure or request it.
Create a callable template def define-reusable-def
<%def name="price_row(label, amount)">
<tr><th>${label}</th><td>${format(amount, '.2f')}</td></tr>
</%def>
<table>${price_row('Subtotal', subtotal)}</table>A `<%def>` compiles into a Python callable and accepts normal parameters and defaults. Its output may be buffered before insertion into the surrounding template.
Fill a block in a parent layout inherit-layout
## detail.html
<%inherit file="/layout.html"/>
<%block name="title">Order ${order.number}</%block>
<p>${order.status}</p>The parent template controls final rendering and decides where `self.body()` appears. File inheritance needs a `TemplateLookup` that can resolve `/layout.html`.
Call defs from a shared namespace import-template-namespace
<%namespace name="forms" file="/shared/forms.html"/>
${forms.text_input(name='email', value=user.email, label='Email address')}Namespace files are resolved through the configured lookup directories. Keep the shared file inside those roots and apply the same escaping policy to its defs.
Separate module setup from per-render work separate-python-blocks
<%!
from decimal import Decimal
def money(value): return f"{Decimal(value):.2f}"
%>
<% total = sum(line.amount for line in invoice.lines) %>
<p>Total ${money(total)}</p>The `<%! ... %>` block runs when the compiled module loads and cannot read render variables. A plain `<% ... %>` block runs once for each render.
Name missing variables in exceptions reject-missing-variable
from mako.template import Template
template = Template(
"Invoice ${invoice_number}",
strict_undefined=True,
)
print(template.render(invoice_number="INV-1042"))`strict_undefined=True` makes a misspelled or absent value raise a direct `NameError`. Without it, the default undefined object produces a less specific failure later.
Map a generated-module error to template lines format-template-traceback
from mako import exceptions
try:
html = lookup.get_template("/report.html").render(report=report)
except Exception:
logger.error(exceptions.text_error_template().render())
raiseThe diagnostic rewrites generated Python frames with template filenames and lines. Do not return `html_error_template()` in production because it can expose source code and render values.
Write through a runtime Context render-to-context
from mako.runtime import Context
with open("report.txt", "w", encoding="utf-8") as stream:
context = Context(stream, rows=rows, title="Inventory")
template.render_context(context)`render_context()` writes to the supplied buffer, but nested defs may still buffer their own output. This API alone does not promise constant-memory streaming.
Keep Alembic revision hooks in script.py.mako customize-alembic-template
## alembic/script.py.mako
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:
${downgrades if downgrades else "pass"}Alembic supplies these names while creating a revision. Test the customized Mako file against the exact Alembic version used by the project before generating production migrations.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| Jinja2 | PyPI | Use it for web templates that should escape HTML by default and expose a more restricted expression language. |
| Chameleon | PyPI | Use it when templates should remain valid HTML or XML with directives stored in attributes. |
| chevron | PyPI | Use Mustache syntax when portable, intentionally logic-light templates are more useful than embedded Python. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

