mrkeyoor.com_
Sun 20 Sept 07:01 UTC
PyPIUtilsupdated 20 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed MakoScreenshot of Mako documentation
Install✓ · 0.2s2 packages on disk · 1 MB
Importimport mako in 0.01s · pure Python · requires Python >=3.10
Known vulns0(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.

API stability5/5Mako 1.x has kept `Template`, `TemplateLookup`, expression syntax, defs, namespaces, inheritance, filters, and compiled-module caching in the same roles. Version 1.4 raises the Python floor and updates packaging without replacing the render API or template grammar. Version 1.4.1 is narrower still: it limits package discovery to `mako` so an unrelated top-level `tools` directory is no longer installed.
Docs5/5The official manual covers syntax, Python blocks, defs, namespaces, inheritance, filters, Unicode, caching, runtime context, exceptions, and integrations with working templates and constructor options. It documents `TemplateLookup` and compiled modules in enough detail to deploy them. Security details are distributed through the design documentation, so readers must connect unrestricted Python execution, opt-in HTML escaping, and caller-controlled template names themselves.
Maintenance4/5GitHub reports 452 stars, 58 open issues and pull requests, an unarchived repository, and a push on August 18, 2026. Releases 1.4.0 and 1.4.1 shipped on consecutive days in August 2026; the second promptly fixed a package-discovery collision. The current series also includes Python 3.15 work and traceback corrections, while earlier 2026 releases closed 2 distinct `TemplateLookup` traversal cases.
Ecosystem4/5PyPI Stats counted 50,268,885 downloads in the latest week. Alembic is a major source of real use because its revision template is `script.py.mako`, and Pyramid and older Pylons applications supply established integrations. Mako remains a narrower direct choice for new web views than Jinja2, so the download total reflects substantial transitive installation and should not be read as 50 million weekly template-engine decisions.

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.
Skip it if

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>
% endif

Every 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())
    raise

The 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

PackageRegistryPick it when
Jinja2PyPIUse it for web templates that should escape HTML by default and expose a more restricted expression language.
ChameleonPyPIUse it when templates should remain valid HTML or XML with directives stored in attributes.
chevronPyPIUse 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.