Plate 71
html.escape vs Manual Replace: Localhost Lab
A hands-on localhost lab comparing html.escape with chained str.replace for safe HTML escaping.
Aditya Challa4 min read
Intro — what this post promises
Escape strings for HTML: html.escape vs chained str.replace for & < > " '. This lab reports ops/s on Linux localhost.
Frame: prefer stdlib html.escape for correctness (entity order, quote flag). Speed is usually in the same band as a careful manual chain. Affiliates: 0.
Related links:
- configparser vs json localhost lab
- zlib vs gzip compress localhost lab
- difflib vs set ops localhost lab
- groupby vs manual localhost lab
- methodcaller vs getattr localhost lab
- queue vs deque handoff localhost lab
- stringio vs list join localhost lab
- hmac compare digest localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. No Docker. Batched inner loops; p50 of 7 rounds.
Verdict up front (light string with specials): html.escape(quote=True) ~3052559 ops/s; manual replace ~2358936. On mixed, both land ~237188 vs ~242654 ops/s. Wrong replace order differs from stdlib (correctness footgun).
Arms
| Arm | Pattern |
|---|---|
html.escape(s, quote=True) | stdlib, escapes quotes |
html.escape(s, quote=False) | leaves quotes |
manual &→<→>→quotes | correct order |
| wrong-order replace | cautionary |
Lab topology
Script: lab-evidence/105-html-escape-vs-manual/results/run_lab.py.
Lead table — light (p50 ops/s)
| Arm | ops/s |
|---|---|
| html.escape quote | 3052559 |
| manual replace quote | 2358936 |
| html.escape noquote | 4682437 |
| manual noquote | 5150710 |
Mixed / long check
| Sample | html.escape quote | manual quote |
|---|---|---|
| mixed | 237188 | 242654 |
| plain | 3884159 | 4285261 |
| long_mixed (23040 chars) | 6203 | 6200 |
Throughput converges when most characters need escaping — allocator/copy work dominates.
Correctness: replace & first
Entities start with &. Replacing & last turns < into &lt;. This lab’s wrong-order arm differed from html.escape (wrong_order_differs=True). Always & first, or just call html.escape.
Attribute vs text context
Text nodes and attributes differ: quotes matter inside attr="...". html.escape(..., quote=True) maps " → " and ' → '. Skipping quote escaping is a common attribute-injection footgun even when < is escaped.
Reading it
- Default to
html.escapein templates/APIs. - Manual chains are fine only if tested against stdlib output.
- Set
quote=Truewhen emitting attribute values. - XSS needs more than escaping (CSP, context-aware encoding) — this lab is micro-escape only.
Why stdlib anyway
Even when manual replace matches today’s CPython output, future quote rules or additional characters are easy to miss in a DIY helper copied across services. Centralizing on html.escape keeps one well-tested path. Microbenchmarks here show you are not leaving large performance on the table by doing so.
Template engines
Many frameworks escape by default in HTML context. Bypassing that with “trusted” manual strings is how XSS slips in. If you must pre-escape, still prefer html.escape and mark the result safe only in the framework’s supported way — not by inventing another replace chain.
Quote flag checklist
Use quote=True (the default in recent Python) whenever the string may land in an HTML attribute. For element text, quote=False skips quote entities and runs a bit faster here (light noquote ~4682437 ops/s vs quote ~3052559), but only choose that when the surrounding context cannot interpret quotes as delimiters.
Not a sanitizer
Escaping special characters is necessary and not sufficient for safe HTML composition. It does not parse DOM, strip event handlers, or neutralize dangerous URLs. Pair escaping with a real policy (CSP, sanitizer library, framework autoescape) rather than treating this microbench as a security design.
Pitfalls
- DIY replace with wrong entity order.
- Escaping twice (double entities) in stacked middleware.
- Using
quote=Falseinside double-quoted attributes. - Trading stdlib for a micro-faster chain you will not maintain.
Reproduce
Evidence: summary.json, summary.txt.
Limits
One Linux box. ASCII-heavy samples. Not full HTML sanitizer / bleach. Not JS or URL encoding.
Takeaway
On light specials, html.escape ~3052559 ops/s vs manual ~2358936 — same league. Prefer html.escape so entity order and quote handling stay correct.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5. light quote=True: html.escape 3052559 ops/s; manual 2358936. mixed ~237188 vs 242654. wrong-order differs. Affiliates: 0. Evidence: lab-evidence/105-html-escape-vs-manual/.
Related links
Plate 12
islice vs list Slice Windows: Localhost Lab
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026
Plate 17
difflib vs set Ops Similarity: Localhost Lab
Hands-on difflib.SequenceMatcher vs set Jaccard token similarity: real ops/s on token lists, measured on Linux localhost in this hands-on lab for SREs.
Observability & SRE · 30 Sept 2026
Plate 63
Decimal vs float Sum: Localhost Lab
Hands-on Decimal vs float cumulative sum lab: real ops/s plus a simple accuracy note (not financial advice), measured on Linux localhost (lab) for SREs.
Observability & SRE · 30 Sept 2026