They're an artifact of the way Python 3 went to Unicode.
Early Python was ASCII-oriented. Type "str" was used both for text strings and arrays of bytes. "str" was treated much like arrays of "char" in C - it was the basic type for binary I/O.
Then came Unicode and Python 3. Characters and bytes were no longer the same thing.
Strings and arrays of bytes had to be split somehow. As with most languages that had to
retrofit Unicode, this didn't go well.
Rust, which didn't have a retrofit problem, did a clean separation. There are arrays of u8,
and there is "str", which must be valid UTF-8 sequences. There is no implicit conversion.
This is straightforward.
Python didn't do it that way. Type "bytes" in Python 3 prints as an ASCII string, not an array of numbers. It's close to the 'str" from Python 2. The usual string-like operations, such as "split" are defined for "bytes". This was kind of weird but was supposed to simplify conversion of old code from Python 2 to Python 3. It didn't really help all that much.
There's also "bytearray", which is a mutable version of "bytes".
It's one of those messes left over from the ASCII to Unicode transition era, along with UTF-16, byte
order marks, "wchar_t", HTML character sets, HTTP headers, and all the character representations in SQL.