Debug Three Planted Bugs in the Mako Python Template Engine
Company: Stripe
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Technical Screen
You are given a working copy of **Mako**, an open-source Python template engine, into which bugs have been planted. Find each bug, fix it with the smallest correct change to the library, and add a regression test for it.
Background: Mako compiles every template into a Python module. It parses the template into a tree of nodes (plain text, `${...}` expressions, `%` control lines such as `% if` and `% endif`, `##` comments, and tags such as `<%def>`), parses the embedded Python with the standard `ast` module, and generates the module's source code from both.
Three bugs are reported. Assume the failing tests show the symptoms described in the three parts below.
### Constraints and Clarifications
- Fix the library, never the tests. You can run the test suite and any snippet you like.
- Treat each symptom as the result of one planted bug, and fix them one at a time.
- Mako can show the Python source it generates for a template; you may use it while debugging.
### Clarifying Questions
- Am I expected to add regression tests, or only to make the existing tests pass?
- When a requested template name matches a directory in one search directory and a file in a later one, should the lookup return the later file?
- Should a control block whose body is only comments render nothing, or should the engine reject it with a clear error?
- Which Python versions must the fixes support? The `ast` module's node types differ between versions.
### Part 1 — Parameters disappear from regenerated code
When Mako turns a parsed Python expression back into source code, the parameter names of a `lambda` are lost: `lambda item: item.name` comes back as a lambda with no parameters, `lambda : item.name`. A template function whose signature has such a default value still compiles, but calling the default fails when the template renders:
```text
<%def name="listing(items, key=lambda item: item.name)">
...
</%def>
```
```hint Compare the tree with the text
Print the parsed tree of `lambda item: item.name` with `ast.dump` and compare it, node by node, with what the generator writes. Find the first node whose output is missing.
```
#### What This Part Should Cover
- How the missing text was traced to the code that handles one kind of `ast` node
- The root cause and the minimal fix
- A regression test that fails before the fix and passes after it
### Part 2 — A directory is taken for a template
A template lookup is configured with several search directories. When the requested template name matches a directory inside one of them, the lookup tries to open that directory as a template and fails with an operating-system error such as `IsADirectoryError`, instead of moving on to the next search directory or raising the lookup's own "template not found" error.
```hint Reread the guard
Find the check the lookup performs before it accepts a candidate path, and list every kind of filesystem entry that passes it.
```
#### What This Part Should Cover
- The check that accepts the wrong candidate, and the corrected check
- Behavior with several search directories, and when no directory has the template
- Related cases: symbolic links, and a file that disappears between the check and the open
### Part 3 — A comment-only block fails to compile
This template fails to compile with a Python syntax error, although a block whose body is only a comment should simply produce no output:
```text
% if show_footer:
## the footer is disabled for now
% endif
```
```hint Compare with blocks that work
Compile the same block with a line of text in its body, and with nothing at all in its body, and see which of the three variants generates valid Python.
```
#### What This Part Should Cover
- Why a body made only of comments produces invalid Python
- The exact rule for when an empty body needs special handling, including `% elif` and `% else` branches
- Regression tests for each variant
### What a Strong Answer Covers
- Reproduces each failure in isolation and inspects intermediate output (the parsed tree, the generated source) before changing code
- Explains each root cause in terms of Python mechanics: `ast` node types and visitor dispatch, filesystem checks, block syntax
- Minimal fixes in the style of the surrounding code, with no test edited to pass
- A regression test per bug, plus the nearby variants of each
- Steady progress across three unrelated areas, with hypotheses narrated while debugging
### Follow-up Questions
- No existing test exercised lambda parameters. What kind of test would catch any node type the source generator mishandles, not just this one?
- A new Python release adds a node type to the `ast` module. Should the generator silently skip a node it has no method for, or fail?
- Should the lookup reject a requested name such as `../config/secrets.txt` that resolves outside its search directories? How would you enforce it?
- A multi-line `<%doc>` comment is the only content of a control block. Does your Part 3 fix cover it?
Overview: A bug-squash exercise on a copy of the Mako Python template engine with planted bugs: lambda parameters vanish from regenerated code, a directory is mistaken for a template during lookup, and a comment-only control block fails to compile. It tests tracing failures through a template compiler, minimal library fixes and regression tests.
Read the full Stripe Software Engineer interview experience this question came from