<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Encoding · XB Field Notes</title><link>https://www.xbcatm.com/tags/encoding/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/encoding/index.xml" rel="self" type="application/rss+xml"/><item><title>UTF-8 preserves text only when byte boundaries and decoding rules agree</title><link>https://www.xbcatm.com/posts/utf8-signals-encoding-in-one-stroke/</link><pubDate>Wed, 02 Dec 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/utf8-signals-encoding-in-one-stroke/</guid><description>Decode a split Chinese and emoji sequence incrementally, reject invalid encodings, and distinguish BOM handling from normalization.</description><content:encoded>&lt;p&gt;UTF-8 encodes Unicode scalar values using one to four bytes. ASCII characters retain their familiar one-byte representation, while many other characters need longer sequences. This makes UTF-8 a useful interchange format, but it does not make arbitrary bytes valid text or every byte offset a character boundary. Correct handling requires an agreed encoding, strict validity rules where appropriate, and a clear distinction between bytes and decoded text.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A file or network read can end halfway through a multibyte sequence. Decoding each chunk independently may reject a perfectly valid complete stream. Replacing errors separately in every chunk is worse when it destroys bytes that would have formed a character with the next read. A streaming decoder needs to retain incomplete input and validate it when more bytes arrive or the stream ends.&lt;/p&gt;
&lt;p&gt;UTF-8&amp;rsquo;s recognizable leading and continuation-byte patterns do not make arbitrary slicing safe. A program that truncates an encoded byte string can cut a sequence in half. Nor does successful decoding under another codec prove that the chosen interpretation is correct. Latin-1 maps every byte, so wrongly decoding UTF-8 as Latin-1 can produce valid Unicode text whose content is nevertheless mojibake.&lt;/p&gt;
&lt;p&gt;Encoding validity is separate from text equivalence and display width. A single visible accented letter can use one code point or a base letter plus a combining mark. Normalization can make selected forms comparable, but decoding UTF-8 does not automatically normalize them. Similarly, a byte-order mark may become a leading text character unless the selected codec intentionally consumes it.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Save this as &lt;code&gt;check-utf8.py&lt;/code&gt; and run &lt;code&gt;python3 check-utf8.py&lt;/code&gt; with Docker. It uses standard-library codecs and Unicode operations in a pinned Python 3.11.16 container without network access. All examples are fixed in-memory bytes and strings. The script performs no database migration or file conversion and does not depend on the terminal correctly displaying an emoji:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;uuid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;program&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;import codecs,json,unicodedata
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;text=&amp;#39;A中🙂&amp;#39;;encoded=text.encode(&amp;#39;utf-8&amp;#39;);assert len(text)==3 and len(encoded)==8
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;assert encoded.hex()==&amp;#39;41e4b8adf09f9982&amp;#39; and encoded.decode(&amp;#39;utf-8&amp;#39;)==text
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;pieces=[encoded[:2],encoded[2:5],encoded[5:]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;split_errors=0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;for piece in pieces:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; try:piece.decode(&amp;#39;utf-8&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; except UnicodeDecodeError:split_errors+=1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;assert split_errors==3
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;decoder=codecs.getincrementaldecoder(&amp;#39;utf-8&amp;#39;)(errors=&amp;#39;strict&amp;#39;);decoded=[]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;for piece in pieces:decoded.append(decoder.decode(piece,final=False))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;decoded.append(decoder.decode(b&amp;#39;&amp;#39;,final=True));assert &amp;#39;&amp;#39;.join(decoded)==text
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;rejected=&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;for label,raw in {&amp;#39;overlong&amp;#39;:bytes.fromhex(&amp;#39;c0af&amp;#39;),&amp;#39;surrogate&amp;#39;:bytes.fromhex(&amp;#39;eda080&amp;#39;),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;too_large&amp;#39;:bytes.fromhex(&amp;#39;f4908080&amp;#39;),&amp;#39;truncated&amp;#39;:bytes.fromhex(&amp;#39;e4b8&amp;#39;)}.items():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; try:raw.decode(&amp;#39;utf-8&amp;#39;);raise AssertionError(&amp;#39;invalid UTF-8 accepted&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; except UnicodeDecodeError as error:rejected[label]=error.reason
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;bad_tail=codecs.getincrementaldecoder(&amp;#39;utf-8&amp;#39;)();assert bad_tail.decode(bytes.fromhex(&amp;#39;e4b8&amp;#39;),final=False)==&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;try:bad_tail.decode(b&amp;#39;&amp;#39;,final=True);raise AssertionError(&amp;#39;incomplete final sequence accepted&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;except UnicodeDecodeError:final_rejected=True
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;mojibake=encoded.decode(&amp;#39;latin-1&amp;#39;);assert mojibake!=text and mojibake.encode(&amp;#39;latin-1&amp;#39;)==encoded
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;decomposed=&amp;#39;e\u0301&amp;#39;;composed=&amp;#39;é&amp;#39;;assert decomposed!=composed and unicodedata.normalize(&amp;#39;NFC&amp;#39;,decomposed)==composed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;bom=b&amp;#39;\xef\xbb\xbf&amp;#39;+encoded;assert bom.decode(&amp;#39;utf-8&amp;#39;).startswith(&amp;#39;\ufeff&amp;#39;) and bom.decode(&amp;#39;utf-8-sig&amp;#39;)==text
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;replacement=bytes.fromhex(&amp;#39;ff41&amp;#39;).decode(&amp;#39;utf-8&amp;#39;,errors=&amp;#39;replace&amp;#39;);assert replacement==&amp;#39;\ufffdA&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;print(json.dumps({&amp;#39;text&amp;#39;:text,&amp;#39;code_points&amp;#39;:len(text),&amp;#39;bytes&amp;#39;:len(encoded),&amp;#39;hex&amp;#39;:encoded.hex(),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;chunk_hex&amp;#39;:[x.hex() for x in pieces],&amp;#39;independent_chunk_failures&amp;#39;:split_errors,&amp;#39;incremental_output&amp;#39;:decoded,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;invalid_rejections&amp;#39;:rejected,&amp;#39;final_incomplete_rejected&amp;#39;:final_rejected,&amp;#39;latin1_code_points&amp;#39;:[hex(ord(c)) for c in mojibake],
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;normalization_lengths&amp;#39;:[len(decomposed),len(composed)],&amp;#39;bom_utf8_first&amp;#39;:hex(ord(bom.decode(&amp;#39;utf-8&amp;#39;)[0])),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#39;utf8_sig_strips_bom&amp;#39;:bom.decode(&amp;#39;utf-8-sig&amp;#39;)==text,&amp;#39;replacement&amp;#39;:replacement},ensure_ascii=False))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;xbcatm-socket-&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;run&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--rm&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--network&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;--memory&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;128m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--pids-limit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;64&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;python3&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;program&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;docker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rm&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The string &lt;code&gt;A中🙂&lt;/code&gt; has three Python string elements representing code points and encodes to eight bytes: &lt;code&gt;41e4b8adf09f9982&lt;/code&gt;. ASCII contributes one byte, the Chinese character three, and the emoji four. The round trip returns the original text. Counting encoded bytes and counting these code points therefore answer different questions; neither should automatically be used as a displayed-column count.&lt;/p&gt;
&lt;p&gt;The bytes are split into &lt;code&gt;41e4&lt;/code&gt;, &lt;code&gt;b8adf0&lt;/code&gt;, and &lt;code&gt;9f9982&lt;/code&gt;. Each piece fails when independently decoded as complete UTF-8. An incremental decoder instead produces &lt;code&gt;A&lt;/code&gt;, then &lt;code&gt;中&lt;/code&gt;, then &lt;code&gt;🙂&lt;/code&gt;, retaining partial sequences between calls. A final empty call with final=True completes validation. A separate decoder holding only &lt;code&gt;e4b8&lt;/code&gt; rejects that incomplete sequence when finalized, rather than silently losing a trailing character.&lt;/p&gt;
&lt;p&gt;Four negative cases reject an overlong encoding, a surrogate code point, a value above the Unicode limit, and an incomplete sequence. Valid UTF-8 excludes those forms even when some bytes resemble plausible continuation patterns. The example catches UnicodeDecodeError specifically. It does not accept an unrelated exception as proof that byte validation worked, and it does not use permissive surrogate handling.&lt;/p&gt;
&lt;p&gt;Decoding the valid UTF-8 bytes as Latin-1 succeeds but yields different code points. In this controlled case, encoding that mistaken string back as Latin-1 recovers the original bytes. That reversible example is not a universal repair method: prior replacement, normalization, or lossy conversion may have discarded information. Preserve original bytes when diagnosing an encoding mismatch before attempting a corrective conversion.&lt;/p&gt;
&lt;p&gt;The script also compares decomposed &lt;code&gt;e&lt;/code&gt; plus a combining acute accent with composed &lt;code&gt;é&lt;/code&gt;. The strings initially differ and have lengths two and one; NFC normalization makes them equal. Separately, plain UTF-8 decoding keeps an initial BOM as U+FEFF, while utf-8-sig consumes it. These are two different transformations, and neither should be applied merely because a text string looks unusual.&lt;/p&gt;
&lt;h2 id="define-the-byte-contract-at-each-boundary"&gt;Define the byte contract at each boundary&lt;/h2&gt;
&lt;p&gt;An HTTP charset declaration or a file-format specification tells a consumer how to interpret bytes; changing the declaration does not transcode the payload. Ensure the producing encoder, transport metadata, parser, and storage interface agree. Test a multibyte sample across the actual boundaries, especially where code truncates values, buffers partial reads, or counts limits expressed in bytes rather than characters.&lt;/p&gt;
&lt;p&gt;Choose error handling according to the data contract. The fixture shows that errors=&amp;lsquo;replace&amp;rsquo; turns invalid input into U+FFFD, a valid but lossy text result. That may suit a diagnostic display, but it cannot prove preservation of identifiers or signed data. Keep rejection, replacement, BOM removal, and normalization as deliberate decisions with different observable effects.&lt;/p&gt;
&lt;p&gt;Finally, use a character-aware operation when limiting decoded text, and a specified framing rule when limiting encoded messages. Incremental decoding solves split byte sequences; it does not define grapheme clusters, line wrapping, or database comparison semantics. Verify the property the application actually requires instead of treating “we use UTF-8” as proof that every text boundary is correct.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc3629.html"&gt;RFC 3629 UTF-8&lt;/a&gt;, valid byte sequences, scalar range and invalid encodings, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.unicode.org/faq/utf_bom.html"&gt;Unicode UTF and BOM FAQ&lt;/a&gt;, encoding boundaries and byte-order-mark behavior, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3.11/library/codecs.html"&gt;Python 3.11 codecs&lt;/a&gt;, incremental decoding, finalization and error policies, checked 2026-09-12.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3.11/library/unicodedata.html"&gt;Python 3.11 unicodedata&lt;/a&gt;, Unicode normalization as a separate operation, checked 2026-09-12.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>