A report turns records back into text that another program will interpret. That creates a new boundary. A URL containing & or a label containing < is ordinary data in Python. If placed directly into HTML, those characters can instead start a character reference or markup. The correct operation depends on the destination format and insertion context.
For HTML text, html.escape encodes the characters that could become markup. For a value inside a quoted HTML attribute, keep quote=True so quotes cannot end the value. Always include the attribute’s surrounding quotes as trusted template syntax. Escape the data once when rendering; keep the original strings in the data model. Repeatedly escaping an already serialized string produces double escaping.
from html import escape
value = 'https://example.test/?q="A&B"'
print(escape(value, quote=True))
print(f'<li data-url="{escape(value, quote=True)}">{escape(value)}</li>')This exercise puts URLs in text and an inert data-url attribute. It does not make them clickable or run a script that navigates to that attribute. HTML escaping would not make a javascript: href acceptable: scheme and destination policy must be considered before creating active links. Nor does this technique make a safe JavaScript expression, CSS value or arbitrary HTML fragment. Choose a serializer or safe API for each actual context.
The report accepts inventory rows with url and nonnegative integer count. These are already valid data records; rendering does not fetch or reclassify them. Sort a new sequence by the original URL string so the same records have the same order regardless of earlier dictionary insertion order. Keep the original list and row dictionaries unchanged. Stable output makes diffs and fixture tests useful without claiming that byte equality alone proves semantic correctness.
JSON has its own serializer. json.dumps handles quotes, backslashes, newlines and Unicode according to JSON syntax. Do not HTML-escape values before encoding JSON: a plain JSON consumer would then receive altered data. Conversely, JSON text is not automatically safe to paste into a browser script element. Our JSON report is a standalone output string, with no script embedding.
import json
rows = [{"url": "https://example.test/é", "count": 2}]
print(json.dumps(rows, ensure_ascii=False, sort_keys=True, separators=(",", ":")))The build fixes formatting deliberately: compact JSON separators, sorted object keys, Unicode characters retained and exactly one final newline. HTML uses a small list template with one element per line and a final newline. Do not add extra wrapping elements, indentation or a generic pretty-printer if exact output is part of the interface. Empty input is a valid empty report, not an error or missing result.
Implement both renderers and compare their outputs for a quote, ampersand, less-than sign, Unicode character and newline inside a URL string. Those test strings are serialization inputs, not a claim that every string is a valid request URL. Also test count zero, empty rows, a malicious-looking title and reversed input order. Keep content validation, destination policy and output serialization separate so each contract is visible and reviewable.