smssafe
Turn arbitrary text into guaranteed GSM-7 deliverable SMS — no UCS-2 fallback.
A single non-GSM-7 character (a smart quote, an em-dash, a stray emoji) forces an entire SMS into UCS-2 encoding: 70 characters per segment instead of 160, triple the cost, and silent delivery failure on feature/dumb phones that can't render it.
smssafe sanitizes text before you send it — transliterating, replacing, or
dropping every non-GSM-7 character while preserving as much meaning as possible.
- ✅ Zero dependencies — pure Python standard library, 3.10+
- ✅ Fully typed — ships
py.typed(PEP 561) - ✅ Homoglyph-aware — Cyrillic/Greek/full-width lookalikes are transliterated, not dropped
- ✅ Accurate segment & cost accounting (extended chars correctly counted as 2)
- ✅ Deterministic, side-effect-free, full audit trail of every change
Install
pip install smssafe
Usage
from smssafe import sanitize
result = sanitize("Hi — “there”, pay ~5,000 now… 🙂")
result.sanitized # 'Hi - "there", pay ~5,000 now...'
result.encoding # 'gsm7'
result.char_count # encoded length (extended chars count as 2)
result.segments # number of SMS parts
result.replacements # list[dict] — audit trail of every substitution
result.remaining_unsafe # list[str] — chars that could not be mapped (dropped)
result.is_clean # True if no changes were needed
What it handles
| Input | Output | Notes |
|---|---|---|
“ ” ‘ ’ smart quotes |
" ' |
Word / Google Docs / AI output |
– — … dash & ellipsis |
- ... |
|
• · ‣ bullets |
- |
|
Cyrillic аеор, Greek Α, full-width A1 |
aeop A A1 |
lookalikes transliterated, not dropped |
ÀÈÌÒÙ uppercase accent traps |
AEIOU |
not in GSM-7 despite lowercase being valid |
™ © ® |
TM (c) (R) |
|
₹ ₽ ₿ |
INR RUB BTC |
(€ £ $ ¥ are kept — they're valid GSM-7) |
| emoji, math-alphanumerics, non-BMP | (stripped) | recorded in remaining_unsafe |
| zero-width / BOM / exotic spaces | (stripped / normalized) |
Tilde:
~is a GSM-7 extended character (escape0x3D, costs 2 septets), so it passes through unchanged —~5,000stays~5,000rather than becoming a misleading-5,000. Non-ASCII tilde lookalikes (˜ ∼ ~) normalise to~.
Drop vs. replace unknowns
By default, characters with no safe mapping are dropped. Pass drop_unknown=False
to replace them with ? instead:
sanitize("A中B", drop_unknown=False).sanitized # 'A?B'
How it works
A deterministic 8-step pipeline (smssafe.core), each step independently testable:
- Strip non-BMP / surrogate codepoints (emoji, math-alphanumerics)
- Apply the homoglyph map (Cyrillic/Greek/full-width → Latin)
- Apply the explicit replacement map (quotes, dashes, currency, symbols…)
- NFD-normalize per character and strip diacritics for remaining accented chars
- Normalize whitespace (tabs, exotic/zero-width spaces)
- Collapse artifacts (runs of dashes/spaces)
- Final GSM-7 scan — drop or
?-replace anything left - Compute encoding, character count (extended = 2), and segment count
Development
git clone https://github.com/BRIQ-BLOCK/smssafe
cd smssafe
pip install -e ".[dev]"
pytest # 689 tests
ruff check .
mypy
License
MIT © Eddie Gulay
Metadata
Release files for smssafe 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| smssafe-0.0.1.tar.gz | 34.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| smssafe-0.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.9 kB
Release files / smssafe-0.0.1.tar.gz
| Download URL | smssafe-0.0.1.tar.gz |
|---|---|
| Size | 34.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
951135c2f760b26bf70cca1dada7b7e9a964879dac6652a51ebaa9e45642fa32
|
|
BLAKE2b-256 checksum How to use checksums |
96aa97f1a59287d4ee9a9f4b4887d2595034a95284328fca13862cd0dcc11a2c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / smssafe-0.0.1-py3-none-any.whl
| Download URL | smssafe-0.0.1-py3-none-any.whl |
|---|---|
| Size | 20.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
746433ea1fa3a635396a35a905432d86891cab581d1983e13c6c9382ba471d85
|
|
BLAKE2b-256 checksum How to use checksums |
0bc031484e768f4944437c5ec02487ab0ad134d0be41002b68b3877f6ec41e9e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|