Python bytes
last modified October 30, 2026 Python 3.15
In this article we show how to use the bytes type in Python.
The bytes type is an immutable sequence of bytes. Each element is
an integer in the range 0 to 255. Text in Python is stored in str
objects, which hold Unicode characters; everything that leaves the program or
enters it from outside, such as files opened in binary mode, network sockets,
pipes, or serial ports, is a sequence of bytes.
The conversion between the two worlds is explicit. A string is turned into
bytes with encode, and bytes are turned into a string with
decode. Both take the name of a character encoding such as UTF-8.
Python also has a mutable counterpart of bytes called
bytearray. Both types share most of their methods with
str.
Creating bytes
There are several ways to create a bytes object.
a = b'falcon'
b = bytes(4)
c = bytes([102, 97, 108, 99, 111, 110])
d = bytes('falcon', 'ascii')
e = 'falcon'.encode()
f = bytes.fromhex('66 61 6c 63 6f 6e')
g = bytes(range(65, 71))
for val in (a, b, c, d, e, f, g):
print(val)
print(a == c == d == e == f)
The example creates the same six bytes in several ways.
a = b'falcon'
A bytes literal is a string literal with the b prefix. It may
contain only ASCII characters.
b = bytes(4)
With an integer argument, bytes creates a zero-filled object of
the given length.
c = bytes([102, 97, 108, 99, 111, 110])
An iterable of integers in the range 0..255 gives one byte per value.
d = bytes('falcon', 'ascii')
e = 'falcon'.encode()
A string must be encoded. We pass the encoding to the bytes
constructor or call the string's encode method, which uses
UTF-8 by default.
f = bytes.fromhex('66 61 6c 63 6f 6e')
g = bytes(range(65, 71))
The fromhex class method parses hexadecimal digits; spaces
between the pairs are ignored. Any iterable of integers works, including a
range.
$ python creating.py b'falcon' b'\x00\x00\x00\x00' b'falcon' b'falcon' b'falcon' b'falcon' b'ABCDEF' True
Encoding and decoding
The number of bytes needed for a string depends on the encoding. The next example encodes a Russian word in four encodings.
word = 'фальконет'
print(word)
print(len(word))
data = word.encode('utf-8')
print(data)
print(len(data))
print(data.decode('utf-8'))
for enc in ('utf-8', 'utf-16-le', 'koi8-r', 'cp1251'):
encoded = word.encode(enc)
print(f'{enc:<10} {len(encoded):>2} bytes {encoded.hex(" ")}')
The word фальконет has nine characters.
data = word.encode('utf-8')
print(data)
print(len(data))
In UTF-8, each Cyrillic letter takes two bytes, so the encoded word has 18
bytes. When bytes are printed, values outside printable ASCII are shown as
\x escapes.
print(data.decode('utf-8'))
Decoding with the same encoding gives the original string back.
for enc in ('utf-8', 'utf-16-le', 'koi8-r', 'cp1251'):
encoded = word.encode(enc)
print(f'{enc:<10} {len(encoded):>2} bytes {encoded.hex(" ")}')
UTF-16 also uses two bytes per letter, but different ones. The legacy Cyrillic encodings KOI8-R and Windows-1251 use one byte per letter. The same text produces completely different bytes; the bytes alone do not tell us how to read them back.
$ python encode_decode.py фальконет 9 b'\xd1\x84\xd0\xb0\xd0\xbb\xd1\x8c\xd0\xba\xd0\xbe\xd0\xbd\xd0\xb5\xd1\x82' 18 фальконет utf-8 18 bytes d1 84 d0 b0 d0 bb d1 8c d0 ba d0 be d0 bd d0 b5 d1 82 utf-16-le 18 bytes 44 04 30 04 3b 04 4c 04 3a 04 3e 04 3d 04 35 04 42 04 koi8-r 9 bytes c6 c1 cc d8 cb cf ce c5 d4 cp1251 9 bytes f4 e0 eb fc ea ee ed e5 f2
Decoding errors
Decoding fails when the bytes are not valid in the chosen encoding. The
errors parameter controls what happens then.
data = 'Dážď'.encode('utf-8')
print(data)
try:
data.decode('ascii')
except UnicodeDecodeError as e:
print(e)
print(data.decode('ascii', errors='replace'))
print(data.decode('ascii', errors='ignore'))
print(data.decode('ascii', errors='backslashreplace'))
print(data[:3].decode('utf-8', errors='replace'))
print(data.decode('cp1250'))
The Slovak word Dážď is encoded in UTF-8 and then decoded in several ways.
try:
data.decode('ascii')
except UnicodeDecodeError as e:
print(e)
ASCII has only 128 values. The first byte above 127 raises a
UnicodeDecodeError, which reports the byte and its position.
This is the default strict error handler.
print(data.decode('ascii', errors='replace'))
print(data.decode('ascii', errors='ignore'))
print(data.decode('ascii', errors='backslashreplace'))
The replace handler puts the U+FFFD replacement character in
place of each bad byte, ignore drops them, and
backslashreplace keeps them as escape sequences.
print(data[:3].decode('utf-8', errors='replace'))
Slicing bytes can cut a multi-byte character in half. The first three bytes hold D, the two bytes of á, and nothing of ž; here the cut happens to fall between characters. Cutting one byte later would leave half of ž, which would be replaced.
print(data.decode('cp1250'))
Decoding with the wrong single-byte encoding never fails, because every byte maps to some character. The result is mojibake: valid but meaningless text.
$ python decode_errors.py b'D\xc3\xa1\xc5\xbe\xc4\x8f' 'ascii' codec can't decode byte 0xc3 in position 1: ordinal not in range(128) D������ D D\xc3\xa1\xc5\xbe\xc4\x8f Dá Dážď
Indexing and slicing
A single element of a bytes object is an integer, while a slice
is another bytes object.
data = b'falcon'
print(data[0])
print(data[-1])
print(data[1:4])
print(data[::-1])
for byte in data[:3]:
print(byte, chr(byte))
print(99 in data)
print(b'lco' in data)
print(list(data))
We access the elements of the b'falcon' object.
print(data[0]) print(data[-1]) print(data[1:4])
Indexing returns 102, the code of the letter f. Slicing returns
b'alc'. This differs from str, where both
operations return strings.
for byte in data[:3]:
print(byte, chr(byte))
Iterating over bytes yields integers; chr turns a value into the
matching character.
print(99 in data) print(b'lco' in data)
The in operator accepts an integer, which tests for a single
byte, or a bytes object, which tests for a subsequence.
$ python indexing.py 102 110 b'alc' b'noclaf' 102 f 97 a 108 l True True [102, 97, 108, 99, 111, 110]
Bytes literals
The representation of bytes uses the literal syntax. Printable ASCII
characters are shown as themselves, a few control characters use escape
sequences such as \n or \t, and all other values use
hexadecimal \xhh escapes.
data = bytes([72, 105, 9, 0, 10, 13, 92, 39, 127, 200, 255])
print(data)
print(len(data))
path = rb'C:\new\table'
print(path, len(path))
text = b'''multi
line'''
print(text)
try:
eval("b'château'")
except SyntaxError as e:
print(e.msg)
The example shows how different values are displayed.
data = bytes([72, 105, 9, 0, 10, 13, 92, 39, 127, 200, 255])
The values 72 and 105 are the letters H and i. 9, 10, and 13 are tab, newline, and carriage return. 92 is the backslash and 39 the single quote; because the data contain a single quote, Python switches the representation to double quotes. 0, 127, 200, and 255 have no printable form.
path = rb'C:\new\table'
The rb prefix creates a raw bytes literal, in which backslashes
are not escape characters.
try:
eval("b'château'")
except SyntaxError as e:
print(e.msg)
Non-ASCII characters are not allowed in bytes literals; such text has to be encoded explicitly.
$ python literals.py b"Hi\t\x00\n\r\\'\x7f\xc8\xff" 11 b'C:\\new\\table' 12 b'multi\nline' bytes can only contain ASCII literal characters
Immutability and bytearray
A bytes object cannot be changed after it is created. When we
need to modify binary data, we use a bytearray.
data = b'falcon'
try:
data[0] = 70
except TypeError as e:
print(e)
buf = bytearray(data)
buf[0] = ord('F')
buf.extend(b' and hawk')
buf += b'!'
print(buf)
del buf[6:]
print(buf)
frozen = bytes(buf)
print(frozen, type(frozen).__name__)
We try to change a byte and then do the same with a bytearray.
try:
data[0] = 70
except TypeError as e:
print(e)
Item assignment on bytes raises a TypeError.
buf = bytearray(data)
buf[0] = ord('F')
buf.extend(b' and hawk')
buf += b'!'
A bytearray supports item assignment, extend,
+=, and other list-like operations.
del buf[6:] print(buf) frozen = bytes(buf)
We delete a slice and then create an immutable copy with
bytes.
$ python immutable.py 'bytes' object does not support item assignment bytearray(b'Falcon and hawk!') bytearray(b'Falcon') b'Falcon' bytes
bytearray.take_bytes in Python 3.15
A common pattern is to collect data in a bytearray and then turn
it into bytes. Calling bytes(buf) copies the whole
buffer. Python 3.15 adds bytearray.take_bytes, which moves the
data out without a copy and leaves the buffer empty.
buf = bytearray()
for i in range(5):
buf += f'line {i}\n'.encode()
print(len(buf))
head = buf.take_bytes(7)
print(head)
print(buf)
rest = buf.take_bytes()
print(type(rest).__name__, len(rest))
print(buf, len(buf))
We build a buffer of five lines and take data out of it.
head = buf.take_bytes(7)
With an argument, take_bytes removes the first n bytes
from the buffer and returns them as a bytes object. The rest
stays in the buffer.
rest = buf.take_bytes()
Without an argument, it takes everything. The buffer is left empty and can be reused.
$ python take_bytes.py 35 b'line 0\n' bytearray(b'line 1\nline 2\nline 3\nline 4\n') bytes 28 bytearray(b'') 0
Bytes methods
Bytes support most string methods. The arguments must be bytes too; Python never mixes text and binary data implicitly.
line = b' GET /index.html HTTP/1.1\r\n'
print(line.strip())
print(line.split())
print(line.strip().startswith(b'GET'))
print(line.upper())
print(line.replace(b'1.1', b'2'))
print(line.find(b'/'))
print(b', '.join([b'sky', b'cloud', b'rock']))
print(b'abc'.center(9, b'*'))
print(b'42'.isdigit(), b'12ab'.isalnum())
try:
line.startswith('GET')
except TypeError as e:
print(e)
try:
b'sky' + 'cloud'
except TypeError as e:
print(e)
We parse a line of an HTTP request.
print(line.strip()) print(line.split()) print(line.strip().startswith(b'GET'))
The strip, split, and startswith
methods work as with strings. Whitespace includes the \r\n
terminator.
print(line.upper()) print(b'42'.isdigit(), b'12ab'.isalnum())
Case conversion and the is* methods consider only ASCII
characters.
try:
line.startswith('GET')
except TypeError as e:
print(e)
try:
b'sky' + 'cloud'
except TypeError as e:
print(e)
Passing a string to a bytes method or concatenating bytes with a string
raises a TypeError.
$ python methods.py b'GET /index.html HTTP/1.1' [b'GET', b'/index.html', b'HTTP/1.1'] True b' GET /INDEX.HTML HTTP/1.1\r\n' b' GET /index.html HTTP/2\r\n' 6 b'sky, cloud, rock' b'***abc***' True True startswith first arg must be bytes or a tuple of bytes, not str can't concat str to bytes
Hexadecimal and binary representations
The hex method and format specifiers show bytes as hexadecimal
or binary digits.
word = 'château'
data = word.encode('utf-8')
print(data)
print(data.hex())
print(data.hex(' '))
print(data.hex(':', 2))
print([f'{b:02x}' for b in data])
print(' '.join(f'{b:08b}' for b in data))
n = int.from_bytes(data)
print(n)
print(bin(n))
back = n.to_bytes((n.bit_length() + 7) // 8)
print(back.decode('utf-8'))
print(bytes.fromhex('63 68 c3 a2 74 65 61 75').decode())
We show the French word château in several representations.
print(data.hex())
print(data.hex(' '))
print(data.hex(':', 2))
The hex method takes an optional separator and the number of
bytes per group.
print([f'{b:02x}' for b in data])
print(' '.join(f'{b:08b}' for b in data))
Since every element is an integer, we can format each one with the
x or b format types.
n = int.from_bytes(data) print(n) print(bin(n)) back = n.to_bytes((n.bit_length() + 7) // 8)
The whole sequence can be read as one big integer and converted back. The number of bytes needed is the bit length rounded up to whole bytes.
print(bytes.fromhex('63 68 c3 a2 74 65 61 75').decode())
The fromhex method reverses hex.
$ python hex_repr.py b'ch\xc3\xa2teau' 6368c3a274656175 63 68 c3 a2 74 65 61 75 6368:c3a2:7465:6175 ['63', '68', 'c3', 'a2', '74', '65', '61', '75'] 01100011 01101000 11000011 10100010 01110100 01100101 01100001 01110101 7163190309837693301 0b110001101101000110000111010001001110100011001010110000101110101 château château
Integers and byte order
The int.to_bytes and int.from_bytes methods convert
between integers and their binary form. The byte order decides whether the
most significant byte comes first (big-endian) or last (little-endian).
n = 1_000_000
big = n.to_bytes(4, 'big')
little = n.to_bytes(4, 'little')
print(big.hex(' '))
print(little.hex(' '))
print(int.from_bytes(big, 'big'))
print(int.from_bytes(little, 'big'))
print((-2).to_bytes(2, signed=True).hex())
print(int.from_bytes(b'\xff\xfe', signed=True))
try:
(300).to_bytes(1)
except OverflowError as e:
print(e)
We encode one million in four bytes.
big = n.to_bytes(4, 'big') little = n.to_bytes(4, 'little')
Network protocols traditionally use big-endian order; x86 and ARM processors store integers in little-endian order. Since Python 3.11, big-endian is the default.
print(int.from_bytes(little, 'big'))
Reading bytes with the wrong byte order gives a completely different number.
print((-2).to_bytes(2, signed=True).hex()) print(int.from_bytes(b'\xff\xfe', signed=True))
Negative numbers need signed=True; they are stored in two's
complement.
try:
(300).to_bytes(1)
except OverflowError as e:
print(e)
A value that does not fit in the given length raises an
OverflowError.
$ python int_bytes.py 00 0f 42 40 40 42 0f 00 1000000 1078071040 fffe -2 int too big to convert
Binary records with struct
The struct module packs several values into bytes according to a
format string and unpacks them again. It is used for binary file formats and
network protocols.
import struct
fmt = '<H10sf?'
print(struct.calcsize(fmt))
records = [
(1, 'Paris'.encode(), 21.5, True),
(2, 'Bratislava'.encode(), 18.25, False),
]
with open('cities.bin', 'wb') as f:
for rec in records:
f.write(struct.pack(fmt, *rec))
with open('cities.bin', 'rb') as f:
raw = f.read()
print(len(raw))
print(raw[:17].hex(' '))
for rid, name, temp, active in struct.iter_unpack(fmt, raw):
print(rid, name.rstrip(b'\0').decode(), temp, active)
We write fixed-size city records to a binary file and read them back.
fmt = '<H10sf?' print(struct.calcsize(fmt))
The format means little-endian (<), an unsigned 2-byte
integer, 10 bytes of text, a 4-byte float, and a 1-byte boolean. Each record
takes 17 bytes.
f.write(struct.pack(fmt, *rec))
The pack function returns bytes. Shorter text is padded with
zero bytes.
for rid, name, temp, active in struct.iter_unpack(fmt, raw):
print(rid, name.rstrip(b'\0').decode(), temp, active)
The iter_unpack function reads one record after another. We
remove the zero padding before decoding the name. The hex dump of the first
record shows the id 01 00, the name, the padding, the float, and
the boolean.
$ python binary_record.py 17 34 01 00 50 61 72 69 73 00 00 00 00 00 00 00 ac 41 01 1 Paris 21.5 True 2 Bratislava 18.25 False
Reading binary files
When we open a file in binary mode, reading returns bytes and no decoding or newline translation takes place.
Dnes bol krásny deň. Dážď bubnoval na okenice.
The text file has a Windows line ending on the first line and a Unix one on the second.
with open('data.txt', 'rb') as f:
lines = f.readlines()
print(lines)
for line in lines:
print(line.decode('utf-8').rstrip())
with open('data.txt', 'rb') as f:
raw = f.read()
text = raw.decode('utf-8')
print(len(raw), 'bytes')
print(len(text), 'characters')
We read the file in rb mode.
with open('data.txt', 'rb') as f:
lines = f.readlines()
The readlines method returns a list of bytes objects. Both line
endings, \r\n and \n, are kept as they are in the
file.
print(len(raw), 'bytes') print(len(text), 'characters')
The file has 53 bytes but 48 characters, because each of the five accented letters takes two bytes in UTF-8.
$ python read_binary.py [b'Dnes bol kr\xc3\xa1sny de\xc5\x88.\r\n', b'D\xc3\xa1\xc5\xbe\xc4\x8f bubnoval na okenice.\n'] Dnes bol krásny deň. Dážď bubnoval na okenice. 53 bytes 48 characters
File signatures
Many binary formats start with a fixed sequence of bytes called a signature or magic number. Reading the first few bytes is enough to recognize the type of a file regardless of its name.
import bz2
import gzip
import sqlite3
import zipfile
with open('data.gz', 'wb') as f:
f.write(gzip.compress(b'hello'))
with open('data.bz2', 'wb') as f:
f.write(bz2.compress(b'hello'))
with zipfile.ZipFile('archive.zip', 'w') as zf:
zf.writestr('a.txt', 'hello')
with sqlite3.connect('test.db') as con:
con.execute('CREATE TABLE t(x)')
con.close()
with open('notes.txt', 'w') as f:
f.write('plain text')
signatures = {
b'\x1f\x8b': 'gzip data',
b'BZh': 'bzip2 data',
b'PK\x03\x04': 'ZIP archive',
b'SQLite format 3\x00': 'SQLite database',
}
def detect(head):
for magic, kind in signatures.items():
if head.startswith(magic):
return kind
return 'unknown'
for name in ('data.gz', 'data.bz2', 'archive.zip', 'test.db', 'notes.txt'):
with open(name, 'rb') as f:
head = f.read(16)
print(f'{name:<12} {head[:8].hex(" "):<24} {detect(head)}')
We create files with the gzip, bz2,
zipfile, and sqlite3 modules and detect their types.
signatures = {
b'\x1f\x8b': 'gzip data',
b'BZh': 'bzip2 data',
b'PK\x03\x04': 'ZIP archive',
b'SQLite format 3\x00': 'SQLite database',
}
Bytes objects are hashable, so they can be dictionary keys. Some signatures are readable ASCII text, others are arbitrary byte values.
with open(name, 'rb') as f:
head = f.read(16)
We read only the first 16 bytes and compare them with
startswith.
$ python signatures.py data.gz 1f 8b 08 00 00 00 00 00 gzip data data.bz2 42 5a 68 39 31 41 59 26 bzip2 data archive.zip 50 4b 03 04 14 00 00 00 ZIP archive test.db 53 51 4c 69 74 65 20 66 SQLite database notes.txt 70 6c 61 69 6e 20 74 65 unknown
Bytes from the network
Data received over the network are bytes. The HTTP response headers usually tell us which encoding to use for decoding.
import urllib.request
with urllib.request.urlopen('https://example.com') as resp:
body = resp.read()
charset = resp.headers.get_content_charset()
print(type(body).__name__, len(body))
print(charset)
print(body[:15])
text = body.decode(charset or 'utf-8')
print(type(text).__name__, len(text))
print(text[:15])
We download the example.com home page with urllib.
body = resp.read() charset = resp.headers.get_content_charset()
The read method returns the body as bytes. The
get_content_charset method extracts the charset parameter from
the Content-Type header.
text = body.decode(charset or 'utf-8')
We decode the body with the announced charset, falling back to UTF-8. The page is pure ASCII, so the number of bytes and characters is the same.
$ python network.py bytes 577 utf-8 b'<!doctype html>' str 577 <!doctype html>
Comparing and sorting bytes
Bytes are compared byte by byte by their numeric values.
words = [b'zebra', b'Zebra', b'apple', b'\xc3\xa1pple', b'Apple']
print(sorted(words))
print(b'abc' < b'abd', b'abc' < b'abcd')
print(b'a' == 'a')
print(b'falcon' == bytearray(b'falcon'))
print(hash(b'sky') == hash(b'sky'))
counts = {b'GET': 3, b'POST': 1}
print(counts[b'GET'])
We sort a list of words and test equality.
print(sorted(words))
Uppercase letters (65..90) sort before lowercase ones (97..122), and the UTF-8 encoded á (starting with byte 0xc3) sorts after all ASCII letters. For human-friendly ordering, decode first and sort strings.
print(b'a' == 'a') print(b'falcon' == bytearray(b'falcon'))
Bytes never compare equal to strings. A bytes and a
bytearray with the same content are equal.
$ python compare.py [b'Apple', b'Zebra', b'apple', b'zebra', b'\xc3\xa1pple'] True True False True True 3
Source
In this article we have worked with the bytes type in Python.
Author
List all Python tutorials.