84

I have some escaped strings that need to be unescaped. I'd like to do this in Python.

For example, in Python 2.7 I can do this:

>>> "\\123omething special".decode('string-escape')
'Something special'
>>> 

How do I do it in Python 3? This doesn't work:

>>> b"\\123omething special".decode('string-escape')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
LookupError: unknown encoding: string-escape
>>> 

My goal is to be able to take a string like this:

s\000u\000p\000p\000o\000r\000t\000@\000p\000s\000i\000l\000o\000c\000.\000c\000o\000m\000

And turn it into:

"[email protected]"

After I do the conversion, I'll probe to see if the string I have is encoded in UTF-8 or UTF-16.

3
  • 1
    Are you absolutely certain those are escapes and not literal bytes? Commented Feb 11, 2013 at 20:47
  • 1
    They are literal bytes! There is a backslash, then a 0, then another 0, then a third 0... I have a program that reads a binary file and outputs information like this. It outputs the binary that is actually in the file. Sometimes the content of the file is UTF-8 coded and it just passes through. But if it isn't valid UTF-8 it gets encoded this way.
    – vy32
    Commented Feb 11, 2013 at 20:48
  • 1
    Same question, but does not specify version. The lowest voted answer there answers for Py3.
    – user202729
    Commented May 12, 2018 at 5:29

6 Answers 6

74

You'll have to use unicode_escape instead:

>>> b"\\123omething special".decode('unicode_escape')

If you start with a str object instead (equivalent to the python 2.7 unicode) you'll need to encode to bytes first, then decode with unicode_escape.

If you need bytes as end result, you'll have to encode again to a suitable encoding (.encode('latin1') for example, if you need to preserve literal byte values; the first 256 Unicode code points map 1-on-1).

Your example is actually UTF-16 data with escapes. Decode from unicode_escape, back to latin1 to preserve the bytes, then from utf-16-le (UTF 16 little endian without BOM):

>>> value = b's\\000u\\000p\\000p\\000o\\000r\\000t\\000@\\000p\\000s\\000i\\000l\\000o\\000c\\000.\\000c\\000o\\000m\\000'
>>> value.decode('unicode_escape').encode('latin1')  # convert to bytes
b's\x00u\x00p\x00p\x00o\x00r\x00t\x00@\x00p\x00s\x00i\x00l\x00o\x00c\x00.\x00c\x00o\x00m\x00'
>>> _.decode('utf-16-le') # decode from UTF-16-LE
'[email protected]'
4
  • That turns my binary object into a Unicode object. I want to keep it a binary object. Any way to do that?
    – vy32
    Commented Feb 11, 2013 at 20:42
  • @vy32: Encode it after decoding? What encoding do you expect this to fit in? ASCII, Latin 1? Commented Feb 11, 2013 at 20:44
  • It could be anything. The program probes a variety of possible codings. It might be ASCII, UTF-8, UTF-16, Latin 1, or a dozen other possibilities.
    – vy32
    Commented Feb 11, 2013 at 20:49
  • 1
    @vy32: Then convert to 'proper' bytes by decoding from unicode_escape, then back to bytes via latin1 (which has the happy coincidence of mapping 1-on-1). You then have bytes to try decodings on. Commented Feb 11, 2013 at 21:01
38

The old "string-escape" codec maps bytestrings to bytestrings, and there's been a lot of debate about what to do with such codecs, so it isn't currently available through the standard encode/decode interfaces.

BUT, the code is still there in the C-API (as PyBytes_En/DecodeEscape), and this is still exposed to Python via the undocumented codecs.escape_encode and codecs.escape_decode.

>>> import codecs
>>> codecs.escape_decode(b"ab\\xff")
(b'ab\xff', 6)
>>> codecs.escape_encode(b"ab\xff")
(b'ab\\xff', 3)

These functions return the transformed bytes object, plus a number indicating how many bytes were processed... you can just ignore the latter.

>>> value = b's\\000u\\000p\\000p\\000o\\000r\\000t\\000@\\000p\\000s\\000i\\000l\\000o\\000c\\000.\\000c\\000o\\000m\\000'
>>> codecs.escape_decode(value)[0]
b's\x00u\x00p\x00p\x00o\x00r\x00t\x00@\x00p\x00s\x00i\x00l\x00o\x00c\x00.\x00c\x00o\x00m\x00'
1
  • 2
    It's a horrible idea to rely on undocumented API. Never ever to this in production code.
    – god
    Commented Aug 10, 2021 at 16:37
28

If you want str-to-str decoding of escape sequences, so both input and output are Unicode:

def string_escape(s, encoding='utf-8'):
    return (s.encode('latin1')         # To bytes, required by 'unicode-escape'
             .decode('unicode-escape') # Perform the actual octal-escaping decode
             .encode('latin1')         # 1:1 mapping back to bytes
             .decode(encoding))        # Decode original encoding

Testing:

>>> string_escape('\\123omething special')
'Something special'

>>> string_escape(r's\000u\000p\000p\000o\000r\000t\000@'
                  r'\000p\000s\000i\000l\000o\000c\000.\000c\000o\000m\000',
                  'utf-16-le')
'[email protected]'
7
  • 3
    Note that if you are OK with always ending up in utf-8, you can do this in a single roundtrip like so: s.encode('latin1', 'backslashreplace').decode('unicode-escape') --- see www.greatytc.com/a/57192592/5583443 Commented Feb 12, 2021 at 3:58
  • @GlenWhitney this doesn't seem to do quite the same thing as decode('string-escape') in python 2 even for UTF-8. e.g. starting with s = '\\xe7\\xa7\\x98', python2 print s.decode('string-escape') prints as I'd hope, and this answer in python3 does the same, but the linked answer to that question prints ç§.
    – James
    Commented Jun 9, 2021 at 18:35
  • @James that probably means you're using a different encoding. What is sys.stdout.encoding? Commented Aug 19, 2021 at 22:31
  • This is good, but the first .encode should be .encode(encoding), otherwise it doesn't work with codepoints > 255. Commented Feb 23, 2023 at 14:57
  • @FHTMitchell: if you do this in the first .encode you should also do it in the 2nd to have a 1:1 mapping, and it won't work in the general case. This solution implies your input string is latin1 or ascii and codepoints > 255 are escaped.
    – MestreLion
    Commented Feb 23, 2023 at 17:09
7

py2

"\\123omething special".decode('string-escape')

py3

"\\123omething special".encode('utf-8').decode('unicode-escape')
2
  • does py2 take the r in front of the string so you don't need to escape the ``?
    – vy32
    Commented Feb 22, 2021 at 22:12
  • 1
    The r prefix means (non-final) backslashes in the string are just literal backslashes. Perhaps that's what you tried to ask but your comment seems botched. It's available in Python 2.7 but I believe it might not be in earlier versions of Python 2. (You should be using Python 3 for any new code in this day and age anyway.)
    – tripleee
    Commented Nov 4, 2021 at 11:33
4

You can't use unicode_escape on byte strings (or rather, you can, but it doesn't always return the same thing as string_escape does on Python 2) – beware!

This function implements string_escape using a regular expression and custom replacement logic.

def unescape(text):
    regex = re.compile(b'\\\\(\\\\|[0-7]{1,3}|x.[0-9a-f]?|[\'"abfnrt]|.|$)')
    def replace(m):
        b = m.group(1)
        if len(b) == 0:
            raise ValueError("Invalid character escape: '\\'.")
        i = b[0]
        if i == 120:
            v = int(b[1:], 16)
        elif 48 <= i <= 55:
            v = int(b, 8)
        elif i == 34: return b'"'
        elif i == 39: return b"'"
        elif i == 92: return b'\\'
        elif i == 97: return b'\a'
        elif i == 98: return b'\b'
        elif i == 102: return b'\f'
        elif i == 110: return b'\n'
        elif i == 114: return b'\r'
        elif i == 116: return b'\t'
        else:
            s = b.decode('ascii')
            raise UnicodeDecodeError(
                'stringescape', text, m.start(), m.end(), "Invalid escape: %r" % s
            )
        return bytes((v, ))
    result = regex.sub(replace, text)
0

At least in my case this was equivalent:

Py2: my_input.decode('string_escape')
Py3: bytes(my_input.decode('unicode_escape'), 'latin1')

convertutils.py:

def string_escape(my_bytes):
    return bytes(my_bytes.decode('unicode_escape'), 'latin1')

Not the answer you're looking for? Browse other questions tagged or ask your own question.