disarm

Whitespace that is not a space

Normalize Unicode whitespace

Unicode has more than twenty characters that render as a gap, and your language calls only some of them whitespace. Braille Pattern Blank and the Hangul fillers look like spaces, sit inside strings like spaces, and survive strip(), trim() and \s untouched. Paste text to see every blank it contains, with codepoints, and get it folded to ordinary spaces.

The tool

0 chars
0 chars

        

Paste text to check it.

Nothing is uploaded. The engine runs inside this page.

Loading the engine…

A blank that is not whitespace

The tool above needs JavaScript. This is the same transformation, written out, so the result is legible without running anything.

Take a username field that rejects blank input and requires the name to be unique. Someone submits — a single Hangul Filler, U+3164. It renders as a gap, so the account displays as nameless. It is not whitespace, so strip() does not empty it and the blank check passes. And because it is one specific codepoint, it is unique: it can be registered again as ㅤㅤ, and again after that.

The same character defeats a moderation filter that matches on admin, because adㅤmin is not that string, and renders close enough to fool a reader.

CharacterCodepoint Renders blankis_whitespace disarm folds it
 U+0020yestrueyes
 U+00A0yestrueyes
 U+3000yestrueyes
U+001Fyesfalseyes
U+2800yesfalseyes
U+115Fyesfalseyes
U+1160yesfalseyes
U+3164yesfalseyes
U+200Bzero widthfalseno — see below

Measured against disarm 0.14.1. The five rows in the middle are the whole argument: they render as a gap, and every whitespace test in the standard library says they are not whitespace. Folding by property misses them; disarm folds by what renders blank.

What it deliberately leaves alone

collapse_whitespace folds whitespace and removes nothing else. A zero-width space, a byte-order mark or a NUL passes straight through it. That is composability rather than an oversight — each cleanup is a separate function, and disarm's own presets chain them: strip_control_chars, then strip_zero_width_chars, then collapse_whitespace. To remove the invisible characters as well, use the invisibles tool.

One more deliberate choice: line controls fold rather than delete, so a\rb becomes a b and never ab. A cleanup that deleted them could silently invent a word that was not in the input.

The same thing in your own code

Each code block has been compiled and verified. Provided under the MIT license to illustrate disarm. disarm on GitHub →

# Fold every blank-rendering character to an ordinary space.
#   pip install disarm
from disarm import collapse_whitespace

# Four characters that all render as a gap. The last two are not whitespace by
# any standard-library test, which is the whole point of this example.
NBSP, IDEO, BRAILLE, FILLER = " ", " ", "⠀", "ㅤ"
BLANKS = (NBSP, IDEO, BRAILLE, FILLER)

text = f"Total{NBSP}due:{IDEO}1,240{BRAILLE}INV{FILLER}0117"

# Printing folded text proves nothing — a gap looks like a gap either way —
# so assert the codepoints are absent instead.
cleaned = collapse_whitespace(text)
for ch in BLANKS:
    assert ch not in cleaned, f"U+{ord(ch):04X} survived"

missed = [c for c in (BRAILLE, FILLER) if not c.isspace()]
assert len(missed) == 2, "expected str.isspace to miss both"

print(f"ok: {len(BLANKS)} blanks folded, {len(missed)} of them invisible to this language's whitespace test")

Where these come from

Nobody types a Braille blank into a form by hand. They arrive by accident far more often than by attack, and both cases end the same way: two strings that look identical do not compare equal.

SourceWhat turns upWhat breaks
Copying out of a PDF or a word processor U+00A0 U+2007 U+202F An exact-match lookup misses. A numeric parse fails on the thousands separator.
Text pasted from a rendered web page U+00A0 U+200B A uniqueness constraint accepts what looks like a duplicate.
CJK input methods and East Asian documents U+3000 U+3164 U+115F A trimmed field is still not empty; a name renders as a blank.
Deliberate padding of a name or username U+2800 U+3164 U+1160 Impersonation by a name that renders the same, and blank-looking accounts.
Machine-generated or legacy records U+001C–U+001F U+0085 Field separators survive into a value and break the next parse downstream.

disarm covers these by class rather than by a hand-written list, so the coverage does not depend on anyone remembering that Braille Pattern Blank exists.

Related tools