Try an automatic code porting tool, like https://python-modernize.readthedocs.io/, to remove #python2 idioms. This is no silver bullet but it helps. For instance, it will detect things now in six.moves (like itertools.imap) or usage of .next() and update your code. On the other hand, it sometimes does useless things like changing "for k, v in d.iteritems():" into "for k, v in six.iteritems(d):".
Every time I see "open(filepath)", I change it into "io.open(filepath, 'rb')" to preserve #python2 behavior. This is fine most of the times; when it's not, it's usually because of some specific business logic you'll have to investigate on anyways. (Similarly, StringIO.StringIO -> io.BytesIO.)
Then, it's often time to dig further into "text versus binary data" issues. Clearly the most painful part of the process because it's often has to do with non-obvious business logic. Things get even worse when third party libraries (e.g. lxml) are in use. https://docs.python.org/3/howto/pyporting.html#text-versus-binary-data
The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!