<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: dark</title>
    <description>The latest articles on DEV Community by dark (@dark_meteor).</description>
    <link>https://dev.to/dark_meteor</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4206135%2Fa3aff6e7-042c-4b9e-aaa9-3bf61ce598df.png</url>
      <title>DEV Community: dark</title>
      <link>https://dev.to/dark_meteor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dark_meteor"/>
    <language>en</language>
    <item>
      <title>Why does copied text look identical but break your code?</title>
      <dc:creator>dark</dc:creator>
      <pubDate>Sun, 25 Oct 2026 07:36:05 +0000</pubDate>
      <link>https://dev.to/dark_meteor/why-does-copied-text-look-identical-but-break-your-code-23pi</link>
      <guid>https://dev.to/dark_meteor/why-does-copied-text-look-identical-but-break-your-code-23pi</guid>
      <description>&lt;p&gt;This is the clean version of the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 👩‍💻
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inserting a zero-width space after &lt;code&gt;count&lt;/code&gt; produces an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  File "demo.py", line 2
    count​ = 2
         ^
SyntaxError: invalid non-printable character U+200B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inserted character is U+200B. It has no visible width, so the two versions look the same. Python rejects it on that line.&lt;/p&gt;

&lt;p&gt;The first Python block deliberately contains no U+200B. To create the failing version, generate the file with explicit escapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"open('demo.py','w').write('total = 10&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;200b = 2&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;print(total / count)  # &lt;/span&gt;&lt;span class="se"&gt;\U&lt;/span&gt;&lt;span class="s2"&gt;0001F469&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;200d&lt;/span&gt;&lt;span class="se"&gt;\U&lt;/span&gt;&lt;span class="s2"&gt;0001F4BB&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;')"&lt;/span&gt;
python3 demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the &lt;code&gt;SyntaxError&lt;/code&gt; above (Python 3.13).&lt;/p&gt;

&lt;p&gt;It doesn't always crash. In a JSON key, the lookup just quietly misses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="se"&gt;\u200b&lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 42}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# None
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="c1"&gt;# ['user\u200bId']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Seeing it
&lt;/h2&gt;

&lt;p&gt;Print the repr instead of the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"print(repr(open('demo.py').read()))"&lt;/span&gt;
&lt;span class="s1"&gt;'total = 10\ncount\u200b = 2\nprint(total / count)  # 👩\u200d💻\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hidden characters show up. &lt;code&gt;\u200b&lt;/code&gt; after &lt;code&gt;count&lt;/code&gt; is the bug. &lt;code&gt;\u200d&lt;/code&gt; is a zero-width joiner. It glues 👩 and 💻 into one "woman technologist" emoji.&lt;/p&gt;

&lt;p&gt;To get line numbers, use GNU grep built with PCRE support. The &lt;code&gt;-P&lt;/code&gt; option is not supported by the grep bundled with macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="s1"&gt;'\x{200B}'&lt;/span&gt; demo.py
2:count​ &lt;span class="o"&gt;=&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Widen the class when needed, e.g. &lt;code&gt;'[\x{200B}\x{00A0}\x{FEFF}]'&lt;/code&gt; for zero-width space, no-break space and BOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't strip everything
&lt;/h2&gt;

&lt;p&gt;A regex that removes every invisible character will fix this file and break other text. Some of these characters are doing real work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;U+200D (zero-width joiner): emoji sequences like 👩‍💻, and shaping in some Indic scripts.&lt;/li&gt;
&lt;li&gt;U+200C (zero-width non-joiner): used in normal Persian writing.&lt;/li&gt;
&lt;li&gt;U+FE0E / U+FE0F (variation selectors): text vs emoji presentation.&lt;/li&gt;
&lt;li&gt;U+00A0 (no-break space): sometimes intended in prose. In code it's almost always a bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So remove only what broke the code. Here that's the U+200B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
s = open('demo.py').read().replace('&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;200b', '')
open('fixed.py', 'w').write(s)
print(repr(s))"&lt;/span&gt;
&lt;span class="s1"&gt;'total = 10\ncount = 2\nprint(total / count)  # 👩\u200d💻\n'&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;python3 fixed.py
5.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs, and the joiner in the emoji is still there.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the browser
&lt;/h2&gt;

&lt;p&gt;I'm dark, and I built &lt;a href="https://invisichar.unboundcove.com/" rel="noopener noreferrer"&gt;InvisiChar&lt;/a&gt; for this. It's free, and the text is processed locally in your browser. Everything above works without it. It's handy when the text isn't in a file and you want to go through findings one at a time.&lt;/p&gt;

&lt;p&gt;For the browser example, use the contents of the generated &lt;code&gt;demo.py&lt;/code&gt;, which includes U+200B. The Inspect screenshot below shows 2 findings: the zero-width space is marked "Suggested removal", and the zero-width joiner is "Protected by default". Nothing changes until you confirm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbh5x2vzjgnqvqoqklw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbh5x2vzjgnqvqoqklw7.png" alt="Inspect results: U+200B suggested for removal, ZWJ protected by default" width="800" height="925"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Clean you pick a preset (Prose, Code &amp;amp; data, International text) and tick the occurrences to remove. The review below selects only U+200B and leaves the joiner unselected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3esrtfrdyd0o6vw7dw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj3esrtfrdyd0o6vw7dw7.png" alt="Cleanup review with only the zero-width space selected" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After applying, it lists the change: one U+200B removed at code-point position 17. The emoji is unchanged.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwqsmfj7iqfachmesw6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwqsmfj7iqfachmesw6h.png" alt="Cleaned output and change list" width="800" height="1117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When identical text behaves differently, check the code points first.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>unicode</category>
      <category>debugging</category>
      <category>python</category>
    </item>
  </channel>
</rss>
