Summary
Python 2.x encouraged many bad habits WRT text handling. In particular, its type named str
does not actually represent text per the Unicode standard (that type is unicode
), and the default "string literal" in fact produces a sequence of raw bytes - with some convenience functions for treating it like a string, if you can get away with assuming a "code page" style encoding.
In 3.x, "string literals" now produce actual strings, and built-in functionality no longer does any implicit conversions between the two types. Thus, the same code now has a TypeError
, because the literal and the variable are of incompatible types. To fix the problem, one of the values must be either replaced or converted, so that the types match.
The Python documentation has an extremely detailed guide to working with Unicode properly.
In the example in the question, the input file is processed as if it contains text. Therefore, the file should have been opened in a text mode in the first place. The only good reason the file would have been opened in binary mode even in 2.x is to avoid universal newline translation; in 3.x, this is done by specifying the newline
keyword parameter when opening a file in text mode.
To read a file as text properly requires knowing a text encoding, which is specified in the code by (string) name. The encoding iso-8859-1
is a safe fallback; it interprets each byte separately, as representing one of the first 256 Unicode code points, in order (so it will never raise an exception due to invalid data). utf-8
is much more common as of the time of writing, but it does not accept arbitrary data. (However, in many cases, for English text, the distinction will not matter; both of those encodings, and many more, are supersets of ASCII.)
Thus:
with open(fname, 'r', newline='\n', encoding='iso-8859-1') as f:
lines = [x.strip() for x in f.readlines()]
# proceed as before
# If the results are wrong, take additional steps to ascertain the correct encoding
How the error is created when migrating from 2.x to 3.x
In 2.x, 'some-pattern'
creates a str
, i.e. a sequence of bytes that the programmer is then likely to pretend is text. The str
type is the same as the bytes
type, and different from the unicode
type that properly represents text. Many methods are offered to treat this data as if it were text, but it is not a proper representation of text. The meaning of each value as a text character (the encoding) is assumed. (In order to enable the illusion of raw data as "text", there would sometimes be implicit conversions between the str
and unicode
types. However, this results in confusing errors of its own - such as getting UnicodeDecodeError
from an attempt to encode, or vice-versa).
In 3.x, 'some-pattern'
creates what is also called a str
; but now str
means the Unicode-using, properly-text-representing string type. (unicode
is no longer used as a type name, and only bytes
refers to the sequence-of-bytes type.) Some changes were made to bytes
to dissociate it from the text-with-assumed-encoding interpretation (in particular, indexing into a bytes
object now results in an int
, rather than a 1-element bytes
), but many strange legacy methods persist (including ones rarely used even with actual strings any more, like zfill
).
Why this causes a problem
The data, tmp
, is a bytes
instance. It came from a binary source: in this case, a file opened with a 'b'
file mode. In other cases, it could come from a raw network socket, a web request made with urllib
or similar, or some other API call.
This means that it cannot do anything meaningful in combination with a string. The elements of a string are Unicode code points (i.e., abstractions that represent, for the most part, text characters, in a universal form that represents all world languages and many other symbols). The elements of a bytes
are, well, bytes. (Specifically in 3.x, they are interpreted as unsigned integers ranging from 0 to 255 inclusive.)
When the code was migrated, the literal 'some-pattern'
went from describing a bytes
, to describing text. Thus, the code went from making a legal comparison (byte-sequence to byte-sequence), to making an illegal one (string to byte-sequence).
Fixing the problem
In order to operate on a string and a byte-sequence - whether it's checking for equality with ==
, lexicographic comparison with <
, substring search with in
, concatenation with +
, or anything else - either the string must be converted to a byte-sequence, or vice-versa. In general, only one of these will be the correct, sensible answer, and it will depend on the context.
Fixing the source
Sometimes, one of the values can be seen to be "wrong" in the first place. For example, if reading the file was intended to result in text, then it should have been opened in a text mode. In 3.x, the file encoding can simply be passed as an encoding
keyword argument to open
, and conversion to Unicode is handled seamlessly without having to feed a binary file to an explicit translation step (thus, universal newline handling still takes place seamlessly).
In the case of the original example, that could look like:
with open(fname, 'r') as f:
lines = [x.strip() for x in f.readlines()]
This example assumes a platform-dependent default encoding for the file. This will normally work for files that were created in straightforward ways, on the same computer. In the general case, however, the encoding of the data must be known in order to work with it properly.
If the encoding is known to be, for example, UTF-8, that is trivially specified:
with open(fname, 'r', encoding='utf-8') as f:
lines = [x.strip() for x in f.readlines()]
Similarly, a string literal that should have been a bytes literal is simply missing a prefix: to make the bytes sequence representing integer values [101, 120, 97, 109, 112, 108, 101]
(i.e., the ASCII values of the letters example
), write the bytes literal b'example'
, rather than the string literal `'example'). Similarly the other way around.
In the case of the original example, that would look like:
if b'some-pattern' in tmp:
There is a safeguard built in to this: the bytes literal syntax only allows ASCII characters, so something like b'ëxãmþlê'
will be caught as a SyntaxError
, regardless of the encoding of the source file (since it is not clear which byte values are meant; in the old implied-encoding schemes, the ASCII range was well established, but everything else was up in the air.) Of course, bytes
literals with elements representing values 128..255 can still be written by using \x
escaping for those values: for example, b'\xebx\xe3m\xfel\xea'
will produce a byte-sequence corresponding to the text ëxãmþlê
in Latin-1 (ISO 8859-1) encoding.
Converting, when appropriate
Conversion between byte-sequences and text is only possible when an encoding has been determined. It has always been so; we just used to assume an encoding locally, and then mostly ignore that we had done so. (Programmers in places like East Asia have been more aware of the problem historically, because they commonly need to work with scripts that have more than 256 distinct symbols, and thus their text requires multi-byte encodings.)
In 3.x, because there is no pressure to be able to treat byte-sequences implicitly as text with an assumed encoding, there are therefore no implicit conversion steps behind the scenes. This means that understanding the API is straightforward: Bytes are raw data; therefore, they are used to encode text, which is an abstraction. Therefore, the .encode()
method is provided by str
(which represents text), in order to encode text into raw data. Similarly, the .decode()
method is provided by bytes
(which represents a byte-sequence), in order to decode raw data into text.
Applying these to the example code, again supposing UTF-8 encoding is appropriate, gives:
if 'some-pattern'.encode('utf-8') in tmp:
and
if 'some-pattern' in tmp.decode('utf-8'):
result = requests.get
and I attempt tox = result.content.split("\n")
. I am a little confused by the error message because it seems to imply thatresult.content
is a string and.split()
is requiring a bytes-like object..?? ( "a bytes-like object is required, not 'str"').. – Antisocial'rb'
option to'r'
to treat the file as a string – Surpassos.write(self.FILE, ":STOP");
, afterself.FILE = os.open("/dev/usbtmc0", os.O_RDWR)
(given the particular hardware is connected through USB, with the right permissions, etc.) – Presentos.write(self.FILE, b":STOP");
. Though it would be better if the why was included here. – Present