Skip to content

Python 3.15: template modules trigger DeprecationWarning from linecache during traceback formatting #437

Closed

Description

@zzzeek

On Python 3.15, formatting a traceback for an exception raised inside a Mako
template emits DeprecationWarning: Module globals is missing a __spec__.loader. Under default warning filters this is silent, but any project
that turns warnings into errors (a very common pytest configuration) sees the
warning raised from inside traceback formatting, which destroys the error
report rather than producing a normal failure.

Reproduction

import warnings, traceback
from mako.template import Template

warnings.simplefilter("error")

try:
    Template("${1/0}").render()
except Exception:
    traceback.format_exc()

On Python 3.15.0b4:

DeprecationWarning: Module globals is missing a __spec__.loader

Remove the simplefilter("error") line and it is silent, but the warning is
still being emitted.

Root cause

_compile_text() compiles in-memory templates into a bare module:

https://github.com/sqlalchemy/mako/blob/main/mako/template.py#L675

module = types.ModuleType(cid)
code = compile(source, cid, "exec")
exec(code, module.__dict__, module.__dict__)

A bare types.ModuleType has both __spec__ and __loader__ present in its
globals, each set to None:

>>> import types
>>> m = types.ModuleType("probe")
>>> m.__dict__.get("__spec__"), m.__dict__.get("__loader__")
(None, None)

Python 3.15 added linecache._bless_my_loader(), which linecache.lazycache()
calls for every frame while a traceback is being built:

def _bless_my_loader(module_globals):
    loader = module_globals.get('__loader__')
    if loader is None and '__spec__' not in module_globals:
        return None                     # not taken: __spec__ IS present
    spec = module_globals.get('__spec__')       # None
    if spec is None and module_globals.get('__name__') == '__main__':
        return loader                   # not taken: __name__ is the template id
    spec_loader = getattr(spec, 'loader', None) # None
    if spec_loader is None:
        import warnings
        warnings.warn(
            'Module globals is missing a __spec__.loader',
            DeprecationWarning)
        return loader

The early return on the second line is the one that would normally protect a
synthetic module, but it only fires when __spec__ is absent. Because
ModuleType puts __spec__ in the dict as None, Mako's template modules
fall through to the warnings.warn() branch.

Walking the traceback frames of a failing render confirms the template module
is the only offender:

        __name__='mako.template'   __spec__=ModuleSpec(... loader=SourceFileLoader ...)
        __name__='mako.runtime'    __spec__=ModuleSpec(... loader=SourceFileLoader ...)
WARN -> __name__='memory:0x7f37607657f0' __spec__=None __loader__present=True

Affected configurations

Three of the four template setups are affected; only the file-backed path with
a module_directory escapes, because that one goes through
compat.load_module() and therefore gets a genuine ModuleSpec.

Configuration default filters warnings as errors
in-memory, no module_directory silent raises
in-memory + module_directory silent raises
file-backed, no module_directory silent raises
file-backed + module_directory silent OK

Impact

No behaviour change for an application running with default warning filters,
since DeprecationWarning is ignored outside __main__.

The problem is for downstream test suites configured with -W error /
filterwarnings = error. Because the warning is raised while the traceback is
being formatted, the original template exception is lost and replaced by the
warning. Under pytest this surfaces as an INTERNALERROR rather than a test
failure, and under pytest-xdist it kills the worker and aborts the entire
run.

Alembic hit exactly this while being tested on 3.15;
tests/test_script_production.py::TemplateArgsTest::test_bad_render renders a
deliberately broken template and takes the whole run down with it. The rest of
that suite passes on 3.15.

Suggested fix

Drop the two empty entries so _bless_my_loader() takes its early return and
treats the template module as carrying no loader information at all:

module = types.ModuleType(cid)
# a bare ModuleType carries __spec__ = None and __loader__ = None, a shape
# that Python 3.15's linecache._bless_my_loader() warns about; remove them
# so it takes its early "no loader information" return instead
module.__dict__.pop("__spec__", None)
module.__dict__.pop("__loader__", None)

Populating __spec__ with a real ModuleSpec would work too, but there is no
meaningful loader for a template compiled from a string, and Mako does its own
source resolution for tracebacks via RichTraceback.

For what it is worth, this is not unique to Mako. Any library that execs into
a bare types.ModuleType produces the same globals shape, so the new check is
arguably aggressive about a common pattern. Given the 3.15 release candidate
phase begins 2026-08-04, it may be worth raising upstream as well if the
early-return condition should also cover __spec__ being present but None.

Environment

  • Python 3.15.0b4
  • Mako 1.3.12 (PyPI) and current main (1.3.13) — _compile_text() is
    unchanged between them, both reproduce

Activity

  1. sqla-tester commented on Aug 3, 2026

    @sqla-tester
    Collaborator

    Mike Bayer has proposed a fix for this issue in the main branch:

    Provide a module spec for string-based template modules https://gerrit.sqlalchemy.org/c/sqlalchemy/mako/+/6859

  2. added a commit that references this issue on Aug 4, 2026
  3. added a commit that references this issue on Aug 4, 2026
    21c11f3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions