Ding ding ding! Somebody has actually identified the reason you cannot migrate from 2 to 3.
In 2 you can read arbitrary bytes into a string without throwing an exception. Only if you try to convert to a Unicode string would an exception be thrown, and you can do lots of stuff with strings without converting them to Unicode (such as read and write the to files and examine the bytes).
In 3 reading into a string can throw an exception if the stream of bytes has an encoding error. The "solution" is that you have to read into a bytes array. But almost certainly what you want to do with the data is pass it to another function that takes a string, and that will throw the exception (either for the wrong data type or because it tried to convert the bytes to a string). You have to rewrite every single function you will call to take a bytes array, rewriting every single thing they call, etc. This is not possible for any reasonable sized software project. It also is really annoying in that 99.99% of the time the data is a "string" in that it is valid UTF-8, and you have thrown away any easy methods of looking at them or comparing them to quoted string constants.
The "string" should have remained a byte array so it could be used for arbitrary bytes, and indexing returns the bytes. "decoding" to Unicode should have been done with iterators, which have the advantage that you can choose the iterator to handle errors in different ways, and to do Unicode normalization if wanted. The "unicode" strings (which are arrays of 16 or 32-bit items) could remain for back-compatibility but deprecated.
Something about Unicode turns otherwise intelligent people into idiot savants, where they will figure out obscenely complex "solutions" for a problem (encoding errors) that should be no more difficult than figuring out how to make your word processor not crash on misspelled words.