Why does Python say my code has an invalid character?
Because an invisible character rode along with a copy-paste and landed somewhere outside a string or comment — a blank space, an identifier, a line of operators — where Python's tokenizer expects a real token and finds a Unicode code point it does not recognize as one. The code looks completely normal in most editors, because the character causing the error has no visible glyph.
Since Python 3.9, the fix for finding it got much easier. Before bpo-40593, CPython's tokenizer just reported a generic invalid syntax at the position. That patch, merged for the 3.9 release, made the tokenizer name the exact code point in the message and put a caret directly under it — turning a guessing game into a one-line diagnosis.
What the error looks like, verified character by character
The examples below were produced by actually running CPython 3.11.15 against a one-line script for each character — not copied from a forum post. Each script assigns a value next to the character in question, outside any string:
| Character | Exact SyntaxError text |
|---|---|
| Zero width space (U+200B) | invalid non-printable character U+200B |
| Byte order mark (U+FEFF) | invalid non-printable character U+FEFF |
| Non-breaking space (U+00A0) | invalid non-printable character U+00A0 |
| Narrow no-break space (U+202F) | invalid non-printable character U+202F |
| Curly left quote (U+2018) | invalid character '‘' (U+2018) |
The last row is the tell for the underlying rule: a code point with no glyph gets the non-printable phrasing, since there's nothing to display next to it; a code point that does render something, like a smart quote, gets shown inside the message itself. Both come from the same check and both name the code point, which is what makes them fixable without guessing.
Why do some invisible characters crash and others don't?
Python only rejects these characters where a real token is expected — outside string literals and comments. Inside a string, the exact same character is accepted silently:
>>> x = "hello\u200bworld" >>> len(x) 11 >>> x == "helloworld" False
No error, no warning — just a string that is one character longer than it looks and fails an equality check that should obviously pass. This is the case worth worrying about more than the SyntaxError: a crash tells you exactly where to look, but a zero-width space sitting inside a string constant, a dictionary key, or a value read from a config file changes program behavior with no diagnostic at all. It's the same class of problem covered in more depth by the guide to sanitizing copied text for code, JSON, and CSV.
How do I fix “SyntaxError: invalid character” right now?
- Read the code point out of the message. Python's traceback already did the hard part — it names the character (U+200B, U+00A0, etc.) and shows a caret under its exact position in the offending line.
- Delete just that character if it's a single stray instance — most editors let you position the cursor right after the caret and press backspace once, even though you cannot see what you're deleting.
- Run the whole file through a character-level cleaner if the error keeps recurring on different lines, which usually means the source you copied from (a PDF, a slide deck, a chat interface) injected the character repeatedly rather than once. Paste the code above and check the report — it lists every hidden character by name and code point before you copy the cleaned version back.
- Re-save the file as plain UTF-8 if a BOM (U+FEFF) was the cause and it keeps reappearing — some editors re-add a BOM on every save unless the encoding is explicitly set to “UTF-8” rather than “UTF-8 with BOM.”
Where these characters actually come from
Almost never a keyboard. The characters in the table above enter Python files through copy-paste from sources that use them for real typesetting reasons that don't survive the trip into plain text: PDFs and justified word processors insert non-breaking and narrow no-break spaces to control line breaks, web pages and Word documents use zero-width spaces as word-break hints inside long tokens like URLs, and files saved by older Windows tools carry a byte order mark by default. AI chat interfaces add their own version of the same problem — see how hidden characters end up in ChatGPT output for what that specific pipeline does. None of it is malicious or unusual; it's just formatting metadata that plain text was never meant to carry, and the invisible character remover strips it in one pass without touching a single visible character, entirely in your browser.