You'd need insane test coverage, of all possible code paths, to be 100% confident.
Most of the conversion difficulties were related to Unicode/text/binary string handling in Python 3, and 2to3 didn't catch all of them. This was the biggest challenge of the entire process. Stuff would fail in production because due to improper string handling. Python 2 was remarkably permissive (i.e. loosy goosey), whereas Python 3 is stricter and arguably more correct, but this strictness has a cost.
The other class of problems is the restructure of certain std libs, like urllib. We took the opportunity to move away from "urllib" to "requests".
This site [1] was invaluable in understanding the migration issues.
All in all, apart from breaking Unicode issues, the migration process was fairly easy.
In the end I decided moving to Rust was less work than Python 3 (in terms of being sure my program was reliable and wouldn't error out with unicode errors).
That means the code was working with raw bytes, not utf-8 strings, so that's what you should convert to on Python3.
That means using `.encode()` and `.decode()` or using bytestrings.
Python3 doesn't break string usage, it makes you do it correctly. Expect a lot more of that kind of pain by moving to Rust (not saying it's not worth, you'll indeed get more correct programs done).
I found it much easier in Rust, as the two types are just distinct, and an incorrect program just fails to compile.