ZetCode

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.

creating.py
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.

encode_decode.py
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.

decode_errors.py
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.

indexing.py
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.

literals.py
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.

immutable.py
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.

take_bytes.py
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.

methods.py
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.

hex_repr.py
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).

int_bytes.py
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.

binary_record.py
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.

data.txt
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.

read_binary.py
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.

signatures.py
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.

network.py
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.

compare.py
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

Python binary sequence types

In this article we have worked with the bytes type in Python.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.