The competition platform from the Paul Newman Cup case study grew a badge this month: a small shield beside each name, with a count of the months that person has beaten the benchmark. A maroon shield, a gold number, a multiplication sign in front of it.

My profile, with one win to its name, showed 21.

Where the 2 came from

The multiplication sign was written as an HTML entity, ×, and joined to the count, so the text for one win was ×1.

The app’s templates autoescape, as they should. Autoescaping turned the & into &, so the browser no longer saw an entity. It saw seven literal characters, ×1, and drew all seven. The shield is narrow, so its outline framed only the middle of that string, and the middle of ×1 is 21.

It looked like a wrong number. It was two digits from the middle of an escaped entity, and the real count was out of frame.

Why my check had passed

I had rendered the badge before shipping it, and it looked right. That sample render used a bare Jinja environment, and a bare environment does not autoescape. Flask switches autoescaping on for HTML templates; Environment() by itself leaves it off. Same template, same input, different output:

from jinja2 import Environment

s = "×1"
Environment().from_string("{{ s }}").render(s=s)
# '×1'        the browser shows a multiplication sign and a 1

Environment(autoescape=True).from_string("{{ s }}").render(s=s)
# '×1'    the browser shows the seven characters

That is jinja2 3.1.6, run while writing this. My lookalike environment was testing a different renderer from the one the site uses, and it agreed with me everywhere except production.

The fix

Use the character itself. The multiplication sign is a single Unicode character, and it needs no entity and no |safe:

In an autoescaping template, an entity in your data is just text. Put the real character in the string and let the escaper do nothing to it.

The test that came with it renders the profile page through the real app, not a stand-in environment, and asserts that no &# survives into the HTML.

Two smaller things the same badge taught me, both about shapes. A first version used a pentagon with a caption inside it, and the caption clipped. And on the shield, the caption sat wonky until it moved beneath the shape: a caption inside a shape needs the shape’s widest part, and a shield has none where the text wants to be.