How to convert bytes to a string in Python
Call .decode() on the bytes: b"hello".decode() gives you "hello". It assumes UTF-8, which is what you want almost every time — pass an encoding, as in data.decode("latin-1"), only when the bytes are something else.
Python keeps bytes and str strictly apart: bytes are raw numbers off a socket, a file or a subprocess, and a string is text. Turning one into the other always needs an encoding — the rulebook mapping characters to numbers — because bytes on their own don't say which one they used. That's the whole story behind the stray b'...' in your logs and the occasional UnicodeDecodeError. Hit Run on any snippet, then edit it and run it again.
1bytes.decode()Recommended
.decode() is the method built for this job. With no arguments it decodes as UTF-8 and returns a real str — one you can slice, format and compare like any other:
Output
The output is Hello, CompileBytes!, then <class 'bytes'> <class 'str'>, then HELLO, COMPILEBYTES!. Notice the quotes are gone from the first line: printing bytes shows b'...', printing a string shows the text. .upper() proves it's a genuine string now — bytes has its own .upper(), but it would hand back more bytes.
2str(data, "utf-8") — and the str(data) trap
The str() constructor decodes too, but only when you give it an encoding. Call it with one argument and it doesn't decode anything — it just prints the bytes object's repr, prefix and all. This is where nearly every "how do I remove the b from my string" question comes from:
Output
Line 1 and line 3 both print b'caf\xc3\xa9'; only the middle one prints café. So str(data, "utf-8") is a fine equivalent of data.decode(), while bare str(data) is the bug — and if a b'...' already ended up in your output, the fix is to decode earlier, not to strip characters off the end.
3Other encodings and broken bytes
When the bytes aren't UTF-8, decoding them as UTF-8 raises UnicodeDecodeError. You have three ways out: name the right encoding, tolerate the bad bytes with errors=, or catch the exception. Here \xe9 is "é" in latin-1, which isn't valid UTF-8 at all:
Output
You get café latte, then caf� latte with the U+FFFD replacement character, then caf latte with the byte silently dropped, then UnicodeDecodeError: invalid continuation byte. Prefer the first route: errors= hides data loss rather than fixing it. latin-1 is the useful last resort because it decodes any byte at all, so it never raises.
4Where the bytes came from
Most bytes reach you from a file opened in "rb" mode, an HTTP response, or a subprocess. Often you don't need to decode by hand at all — json.loads() accepts bytes directly, and read_text() decodes for you:
Output
Prints the raw b'{"tool": "CompileBytes"}', then CompileBytes, then the decoded text. Same idea elsewhere: subprocess.run(..., text=True) gives you str instead of bytes on .stdout, and response.text is the decoded version of response.content in requests.
5Which should you use?
| Method | Result | Best for |
|---|---|---|
| data.decode() | str, UTF-8 | The default — almost everything |
| data.decode("latin-1") | str, never raises | Legacy or unknown byte streams |
| data.decode("utf-8", errors="replace") | str with � | Logging junk you must not crash on |
| str(data, "utf-8") | str, UTF-8 | Identical to .decode(); style choice |
| str(data) | b'...' repr | Nothing — this is the classic mistake |
| Path(...).read_text() | str, decoded | Reading a text file — skip bytes entirely |
6Going the other way: str.encode()
.encode() is the mirror image, and it's what you need before writing to a socket or a binary file. It's also the clearest way to see that characters and bytes aren't the same thing:
Output
The first line prints b'caf\xc3\xa9' 4 5 — four characters, five bytes, because "é" takes two of them in UTF-8. The rest shows the same .decode() works on a bytearray, and that raw byte values decode just as well: [72, 105] is Hi.
Frequently asked questions
Why does my string print with a b in front of it?
That b'...' isn't part of the text — it's how Python shows a bytes object, the same way quotes show a string. It means you printed bytes without decoding them. Decode at the point the data arrives (data.decode()), rather than stripping the prefix off the printed output afterwards.
What is the difference between .decode() and str()?
data.decode() and str(data, "utf-8") do exactly the same thing. The difference is bare str(data) with no encoding: that does not decode at all, it just returns the repr b'...'. Prefer .decode() — it can't be called wrongly by accident.
How do I fix UnicodeDecodeError: invalid start byte?
The bytes aren't UTF-8. Find the real encoding and pass it — data.decode("latin-1"), "cp1252" and "utf-16" cover most Windows and legacy sources. If you genuinely can't know, data.decode("utf-8", errors="replace") keeps the run alive by substituting � for the bad bytes, and latin-1 decodes any byte sequence without raising.
How do I know which encoding the bytes use?
Nothing in the bytes themselves tells you — you have to get it from context: an HTTP Content-Type header, an XML/HTML declaration, or the documentation of whatever produced them. UTF-8 is the right first guess for anything modern. For a genuine unknown, the charset-normalizer or chardet packages guess statistically.
How do I convert hex or base64 bytes to a string?
Those are two steps. bytes.fromhex("4869") gives you b'Hi', and base64.b64decode("SGk=") also gives you b'Hi' — both return bytes, so you still call .decode() to get "Hi". Going out, b"Hi".hex() is '4869' and base64.b64encode(b"Hi").decode() is 'SGk='.
Why is len() different for bytes and the string?
len() on a string counts characters; on bytes it counts bytes. In UTF-8 an ASCII character is one byte but accented letters take two, and emoji take four — so len("café") is 4 while len("café".encode()) is 5. Slice the string, never the bytes, or you can cut a character in half.