ftfy(1)
| FTFY(1) | ftfy | FTFY(1) |
NAME
ftfy - ftfy Documentation
Version 6.3
“Assume all external input is the result of (a series of) bugs.” — RFC 9225 <https://www.rfc-editor.org/rfc/rfc9225.html>: Software Defects Considered Harmful
ftfy fixes Unicode that's broken in various ways.
The goal of ftfy is to take in bad Unicode and output good Unicode, for use in your Unicode-aware code.
This is different from taking in non-Unicode and outputting Unicode, which is not a goal of ftfy. It also isn't designed to protect you from having to write Unicode-aware code. ftfy helps those who help themselves.
Of course you're better off if your input is decoded properly and has no glitches. But you often don't have any control over your input; it's someone else's mistake, but it's your problem now. ftfy will do everything it can to fix the problem.
ftfy is a heuristic that was designed (not machine-learned) by Robyn Speer. If you use ftfy in research, including pre-processing your language model data, you need to cite it: see Citing ftfy <#cite>.
FIXING PROBLEMS AND GETTING EXPLANATIONS
Ode to a Shipping Label
A poem about mojibake <https://imgur.com/4J7Il0m>, whose original author might be Carlos Bueno on Facebook <https://www.facebook.com/cmb/posts/619241744770551:0>, shows a shipping label that serves as an excellent example for this section, addressed to the surname L&AMP;ATILDE;&AMP;SUP3;PEZ. [image: A package addressed to a name including "L&AMP;ATILDE;&AMP;SUP3;PEZ"] [image]
We can use ftfy not only to fix the text that was on the label, but to show us what happened to it (like the poem does):
>>> from ftfy import fix_and_explain, apply_plan
>>> shipping_label = "L&AMP;ATILDE;&AMP;SUP3;PEZ"
>>> fixed, explanation = fix_and_explain(shipping_label)
>>> fixed
'LóPEZ'
>>> explanation
[('apply', 'unescape_html'),
('apply', 'unescape_html'),
('apply', 'unescape_html'),
('encode', 'latin-1'),
('decode', 'utf-8')]
The capitalization is inconsistent because the encoding of a lowercase "ó" is in there, but everything was printed in capital letters.
The explanation may even be able to be applied to different text with the same problem:
>>> label2 = "CARR&AMP;ATILDE;&AMP;COPY;" >>> apply_plan(label2, explanation) 'CARRé'
Functions that fix text
The function that you'll probably use most often is ftfy.fix_text(), which applies all the fixes it can to every line of text, and returns the fixed text.
ftfy.fix_and_explain() takes the same arguments as ftfy.fix_text(), but provides an explanation, like we saw in the first section.
Unlike ftfy.fix_text(), ftfy.fix_and_explain() doesn't separate the text into lines that it fixes separately -- because it's looking for a unified explanation of what happened to the text, not a different one for each line.
A more targeted function is ftfy.fix_encoding_and_explain(), which only fixes problems that can be solved by encoding and decoding the text, not other problems such as HTML entities:
This function has a counterpart that returns just the fixed string, without the explanation. It still fixes the string as a whole, not line by line.
The return type of the ..._and_explain functions is a kind of NamedTuple called ExplainedText:
These explanations can be re-applied to text using apply_plan():
Showing the characters in a string
A different kind of explanation you might need is simply a breakdown of what Unicode characters a string contains. For this, ftfy provides a utility function, ftfy.explain_unicode().
A command-line utility that provides similar information, and even more detail, is lunasorcery's utf8info <https://github.com/lunasorcery/utf8info>.
CONFIGURING FTFY
The main functions of ftfy -- ftfy.fix_text() and ftfy.fix_and_explain() -- run text through a sequence of fixes. If the text changed, it will run them through again, so that you can be sure the output ends up in a standard form that will be unchanged by ftfy.
All the fixes are on by default, but you can pass in a configuration object or keyword options to turn them off. Check that the default fixes are appropriate for your use case. For example:
- You should set unescape_html to False if the output is meant to be interpreted as HTML.
- You should set fix_character_width to False if you want to preserve the spacing of CJK text.
- You should set uncurl_quotes to False if you want to preserve quotation marks with nice typography. You could even consider doing the opposite of uncurl_quotes, running smartypants <http://pythonhosted.org/smartypants/> on the result to make all the punctuation typographically nice.
- To be cautious and only fix mojibake when it can be fixed with a consistent sequence of encoding and decoding steps, you should set decode_inconsistent_utf8 to False.
If the only fix you need is to detect and repair decoding errors (mojibake), use the ftfy.fix_encoding() function directly. However, note that mojibake is often entangled with other issues such as the curliness of quotation marks, so limiting the process to this step might make some mojibake unfixable.
The TextFixerConfig object
The top-level functions of ftfy take a config argument that is an instance of ftfy.TextFixerConfig. If this argument is None, the configuration will use its default values.
Keyword arguments
The top-level functions also accept keyword arguments in place of a config argument. Given these keyword arguments, they will pass them to the ftfy.TextFixerConfig constructor, overriding the default values of those configuration options.
ENCODINGS FTFY CAN HANDLE
ftfy can't fix all possible mix-ups. Its goal is to cover the most common encoding mix-ups while keeping false positives to a very low rate.
ftfy can understand text that was decoded as any of these single-byte encodings:
- Latin-1 (ISO-8859-1)
- Windows-1250 (cp1250 -- used in Microsoft products in Eastern Europe)
- Windows-1251 (cp1251 -- used in Microsoft products in Russia)
- Windows-1252 (cp1252 -- used in Microsoft products in Western Europe and the Americas)
- Windows-1253 (cp1253 -- used in Microsoft products in Greece)
- Windows-1254 (cp1254 -- used in Microsoft products in Türkiye)
- Windows-1257 (cp1257 -- used in Microsoft products in Baltic countries)
- ISO-8859-2 (which is not quite the same as Windows-1250)
- MacRoman (used on Mac OS 9 and earlier)
- cp437 (it's the "text mode" in your video card firmware)
when it was actually intended to be decoded as one of these variable-length encodings:
- UTF-8
- CESU-8 (a common, incorrect implementation of UTF-8)
It can also understand text that was intended as Windows-1252 but decoded as Latin-1. That's the very common case where things like smart-quotes and bullets turn into single weird control characters.
However, ftfy cannot understand other mixups between single-byte encodings, because it is extremely difficult to detect which mixup in particular is the one that happened.
We also can't handle the legacy encodings used for Chinese, Japanese, and Korean, such as shift-jis and gb18030. See issue #34 <https://github.com/rspeer/python-ftfy/issues/34> for why this is so hard.
I tried adding support for cp850, the cp437-workalike that supported European languages, but I couldn't find any real examples that it fixed, and it introduced some false positives.
Remember that the input to ftfy is Unicode, so it handles actual CJK text just fine. It just can't discover that a CJK encoding introduced mojibake into the text.
FIXER FUNCTIONS
IS FTFY AN ENCODING DETECTOR?
No, it's a mojibake detector (and fixer). That makes its task much easier, because it doesn't have to guess the encoding of everything: it can leave correct-looking text as it is.
Encoding detectors have ended up being a bad idea, and they are largely responsible for creating the problems that ftfy has to fix.
The text that you put into ftfy should be Unicode that you've attempted to decode correctly. ftfy doesn't accept bytes as input.
There is a lot of Unicode out there that has already been mangled by mojibake, even when decoded properly. That is, you might correctly interpret the text as UTF-8, and what the UTF-8 text really says is a mojibake string like "réflexion" that needs to be decoded again. This is when you need ftfy.
I really need to guess the encoding of some bytes
I understand. Sometimes we can't have nice things.
Though it's not part of the usual operation of ftfy, ftfy does contain a byte-encoding-guesser that tries to be less terrible than other byte-encoding-guessers in common cases. Instead of using probabilistic heuristics, it picks up on very strong signals like "having a UTF-16 byte-order mark" or "decoding successfully as UTF-8".
This function won't solve everything. It can't solve everything. In particular, it has no capacity to guess non-Unicode CJK encodings such as Shift-JIS or Big5.
HOW CAN I AVOID PRODUCING MOJIBAKE?
Read the Python Unicode HOWTO
The Python Unicode HOWTO <https://docs.python.org/3/howto/unicode.html> is a useful introduction to how to use Unicode correctly in Python. If you find yourself confused about the difference between bytes and characters, or you need to unlearn bad habits from Python 2, it's a great place to start.
Assume UTF-8
Assume text is in UTF-8 unless you have a specific reason to believe it isn't.
In the 2020s, UTF-8 is everywhere <http://utf8everywhere.org/>, especially in text meant to be transferred over the Internet. Most mojibake comes from decoding correct UTF-8 as if it were some other encoding.
In Python 3, you should use the Unicode string type (str) for all operations. You should open text files in UTF-8:
openfile = open(filename, encoding='utf-8', errors='replace')
When you are specifically working with bytes and you need to turn them into text, you should decode them as UTF-8:
text = bytebuffer.decode('utf-8', 'replace')
The exceptions, the cases where you're not using UTF-8, are few but relevant. If you're interacting with C APIs, you'll need to represent your text as bytes in the format the API expects. Windows APIs in particular expect UTF-16.
We're mostly past the dark days when encodings were "character maps" of 256 possible characters, one byte per character. An unfortunate thing that keeps them alive is Microsoft Excel, whose "Export" feature will pick a 256-character encoding based on your computer's operating system and default language. So:
Don't export CSVs from Excel
I know that I'm telling you not to do something that may seem like a requirement of doing your job. But don't export CSV files from Excel if you have any other choice. Though Excel CSVs look right on basic ASCII characters, on any other text, it either won't work or won't do what you want. Excel CSVs aren't even interoperable between different computers.
My recommendation is to use Google Sheets to create CSVs, and keep Excel files in .xlsx format so the Unicode won't be mangled.
If you must export a CSV-like file from Excel, you can find an option to tell Excel to export in "Unicode Text", and it will create a tab-separated UTF-16 file. This is not a very widely-used format, but at least it's not mojibake.
You can follow these unwieldy directions from a SalesForce help article <https://help.salesforce.com/articleView?id=000324657&type=1&mode=1> to use Excel and Notepad to create a UTF-8 CSV. You can see why I don't recommend this process.
Don't use chardet
Encoding detection on raw bytes is not a good idea. It was important in the '90s, during the rough transition to Unicode -- and the most popular way of doing it, chardet, hasn't changed since the '90s.
A heuristic designed before there was multilingual social media, before there were emoji, is not going to work correctly in the 2020s.
When chardet sees the correct UTF-8 encoding of an emoji, it will have no idea what it's looking at, because it won't match anything in its training data. Often, it will guess that it's Turkish encoded in Windows-1254. On other reasonable text, it will guess the "iso-8859-2" encoding, an encoding that you'd very rarely see used intentionally. Because the problem is inherent to the design of chardet, it's not easily fixed.
chardet was built on the assumption that "encoding detection is language detection", which is no longer true. Web sites now contain text in multiple languages, and for the most part they use UTF-8 regardless of the language.
I've strengthened my recommendation from "don't trust chardet's output" to "don't use chardet", because there's no realistic way to use chardet without trusting its output. We've reached a situation where major Python packages such as requests assume that chardet is correct, and yet the changing nature of text means that chardet is more wrong with each passing year.
So how should you interpret raw bytes of text if you're not told what encoding they're in? As UTF-8. Text is UTF-8 until proven otherwise.
ASCII isn't extended
A sign that something is about to go wrong with encodings is if a developer is talking about "extended ASCII".
ASCII is a set of 128 character codes (95 of them displayable). It has not had any new characters added to it since the backslash was added in 1967.
Because ASCII is a 7-bit encoding but our computers use 8-bit bytes, it seems clear that ASCII could be extended to assign a meaning to all 256 possible bytes. There are many different encodings that have done so, and they're all incompatible with one another, which is why treating bytes as characters as a bad idea and why we have Unicode now.
Many developers refer to one of these encodings as "extended ASCII", whose colloquial meaning is "the encoding of 256 characters that I learned first". Its meaning is completely dependent on the country you were in and the operating system you were using when you started programming:
- My "extended ASCII" when I learned to program was IBM codepage 437, the one that was used in US versions of MS-DOS.
- To many people, "extended ASCII" is Windows codepage 1252, which they'd find in the Character Map of their Windows 9x computer, at least if they were in North America or Western Europe.
- To others in other countries, it could be a different Windows codepage, such as 1251 (which contains Cyrillic letters) or 1250 (which contains a different set of accented letters for Eastern European languages).
- Or it might be Latin-1, the common name for the ISO-8859-1 standard that became the first 256 characters of Unicode. Latin-1 is easy to implement by accident, such as when you see byte C2 and assume it means Unicode codepoint U+00C2 -- what you get by incorrectly running chr() on each byte.
"Extended ASCII" doesn't specify which encoding you mean, and often indicates that you don't realize that different people are thinking of different sets of 256 characters.
Instead of "extended ASCII", say the name of the encoding such as "Latin-1", "Windows-1252", "Windows-1250", "codepage 437", or maybe "I don't know what it is but it looks right on my machine".
And then revise things so that you use UTF-8, which is still a superset of ASCII but can represent every Unicode character.
HEURISTICS FOR DETECTING MOJIBAKE
The "badness" heuristic
The "UTF-8 detector" heuristic
A more narrow heuristic is defined in chardata.py as UTF8_DETECTOR_RE. This heuristic looks for specific sequences of mojibake characters that come from the decoding of common UTF-8 sequences.
Text that matches this regular expression can be partially fixed by ftfy.fixes.decode_inconsistent_utf8(), even when the string as a whole doesn't decode consistently.
Because of this, the expression requires that the match isn't preceded by likely UTF-8 characters -- if this were allowed, then it might pick two or three characters out of a larger mess of mojibake to decode as another character while leaving the rest untouched. This makes the problem more confusing, doesn't really solve anything, and can even pile up recursively to decode as entirely arbitrary characters.
The "lossy UTF-8" heuristic
chardata.py also includes LOSSY_UTF8_RE, which is used similarly to the "UTF-8 detector" heuristic. This regular expression matches sequences that look like they were incorrectly decoded from UTF-8, but with characters replaced by question marks or the Unicode replacement character �.
Characters that match this heuristic will be replaced by � in the ftfy.fixes.replace_lossy_sequences() fixer.
SUPPORT FOR BAD ENCODINGS
"Sloppy" encodings
Variants of UTF-8
COMMAND-LINE USAGE
ftfy can be used from the command line. By default, it takes UTF-8 input and writes it to UTF-8 output, fixing problems in its Unicode as it goes.
Here's the usage documentation for the ftfy command:
usage: ftfy [-h] [-o OUTPUT] [-g] [-e ENCODING] [-n NORMALIZATION]
[--preserve-entities]
[filename] ftfy (fixes text for you), version 6.0 positional arguments:
filename The file whose Unicode is to be fixed. Defaults to -,
meaning standard input. optional arguments:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
The file to output to. Defaults to -, meaning standard
output.
-g, --guess Ask ftfy to guess the encoding of your input. This is
risky. Overrides -e.
-e ENCODING, --encoding ENCODING
The encoding of the input. Defaults to UTF-8.
-n NORMALIZATION, --normalization NORMALIZATION
The normalization of Unicode to apply. Defaults to
NFC. Can be "none".
--preserve-entities Leave HTML entities as they are. The default is to
decode them, as long as no HTML tags have appeared in
the file.
CITING FTFY
ftfy has been used as a data processing step in major NLP research, including OpenAI's original GPT.
It's important to give credit appropriately to everyone whose work you build on in research. This includes software, not just high-status contributions such as mathematical models. All I ask when you use ftfy for research is that you cite it.
ftfy has a citable record on Zenodo <https://zenodo.org/record/2591652>. A citation of ftfy may look like this:
In BibTeX format, the citation is:
@misc{speer-2019-ftfy,
author = {Robyn Speer},
title = {ftfy},
note = {Version 5.5},
year = 2019,
howpublished = {Zenodo},
doi = {10.5281/zenodo.2591652},
url = {https://doi.org/10.5281/zenodo.2591652}
}
SOME QUICK EXAMPLES
Here are some examples (found in the real world) of what ftfy can do:
ftfy can fix mojibake (encoding mix-ups), by detecting patterns of characters that were clearly meant to be UTF-8 but were decoded as something else:
>>> import ftfy
>>> ftfy.fix_text('✔ No problems')
'✔ No problems'
Does this sound impossible? It's really not. UTF-8 is a well-designed encoding that makes it obvious when it's being misused, and a string of mojibake usually contains all the information we need to recover the original string.
ftfy can fix multiple layers of mojibake simultaneously:
>>> ftfy.fix_text('The Mona Lisa doesn’t have eyebrows.')
"The Mona Lisa doesn't have eyebrows."
It can fix mojibake that has had "curly quotes" applied on top of it, which cannot be consistently decoded until the quotes are uncurled:
>>> ftfy.fix_text("l’humanité")
"l'humanité"
ftfy can fix mojibake that would have included the character U+A0 (non-breaking space), but the U+A0 was turned into an ASCII space and then combined with another following space:
>>> ftfy.fix_text('Ã\xa0 perturber la réflexion')
'à perturber la réflexion'
>>> ftfy.fix_text('à perturber la réflexion')
'à perturber la réflexion'
ftfy can also decode HTML entities that appear outside of HTML, even in cases where the entity has been incorrectly capitalized:
>>> # by the HTML 5 standard, only 'PÉREZ' is acceptable
>>> ftfy.fix_text('P&EACUTE;REZ')
'PÉREZ'
These fixes are not applied in all cases, because ftfy has a strongly-held goal of avoiding false positives -- it should never change correctly-decoded text to something else.
The following text could be encoded in Windows-1252 and decoded in UTF-8, and it would decode as 'MARQUɅ'. However, the original text is already sensible, so it is unchanged.
>>> ftfy.fix_text('IL Y MARQUÉ…')
'IL Y MARQUÉ…'
Author
Robyn Speer
Copyright
2024, Robyn Speer
| February 3, 2026 | 6.3 |
