Translating a Python2 App to Python3 in 2025, Part 1: Source Code
The Python logo with version 2.7 crossed out and replaced by version 3.12

Translating a Python2 App to Python3 in 2025, Part 1: Source Code

Some of you may have seen my recent post announcing that I am beginning a personal project to modernize the DTOcean software. For anyone not in the know, DTOcean is a design tool (hence DT) for arrays of marine (the 'ocean' bit) renewable energy devices. It can calculate a realistic design for a given location and then assess its energy production, cost and environmental impact, and comes packaged as a desktop graphical application, all created using Python. It also began development in 2014, which means it's written in Python2. And by 2021, when I last updated the code, maintaining a Python2 program had already become virtually impossible.

I thought it might be interesting, then, to share how, in 2025, I am refactoring a large Python2 project to Python3. The DTOcean GitHub organization has 23 repositories that make up the full application, so this is no small feat. Fortunately, improvements in tooling, for source code and packaging, have made the task less daunting than it was 4 years ago. The next article will discuss how packaging Python applications has changed, while this article is dedicated to describing how modern tools are helping me update the source code efficiently.

Updating Source Code

The first requirement when converting from Python2 to Python3 is to make the source code syntactically correct. The most well known syntax change was print, which changed from a statement to a function. It's not the only one, however, with changes like removal of the L suffix (which indicated a digit should be cast to the now removed long type), and to exceptions. A fairly comprehensive list of syntax changes is available on The Conservative Python 3 Porting Guide, from which we can see that a simple Ctrl-F is not going to be sufficient.

Note that the porting guide was last updated 4 years ago, and is based on converting to (long end-of-life) Python 3.6. It also recommends tools for conversion, such as python-modernize, which is now 5 years from its last update. Even if python-modernize is still working, this tool uses the six package, which was designed to allow code to run in both Python 2 and 3; although it's currently still supported, who knows for how long that will last? Add to that that the standard library conversion tool, 2to3, has been removed as of Python 3.13, are there any other modern tools that can help?

Fear not, mad few who, like me, want to update a Python2.7 program in 2025, because static analysis tools in Python are now amazingly useful. These come in two flavours called 'linters' and 'type checkers', and I'll explain how I use these below.

Linting (but first Formatting)

Before I discuss linting, I need to do a quick segue into formatting of Python code. Python has had an "active" code style guide for a long time, defined in 2001 by the famous PEP 8. This is just a guide, however, and there remains plenty of room for personal preference. Over the years, I have got much more pernickety about the format of my code (I always have whitespace markers showing in my editors, for instance), and I gained a strong belief that my way was the right way. Nonetheless, this attitude is a recipe for pain when working in a team, as I found out to my discomfort when one of my bosses wanted it to be done THE WRONG WAY, AHGHFH! Solution? Let a third party format the code the same way for everyone, as Go has been doing since 2013. For Python, this became popular with the black package, but the modern equivalent that I've been using is...

Ruff is a code formatting and linting tool for Python that is written in the Rust language. It's an amalgamation of the functions of lots of other linting tools like Pylint, bandit, etc, which analyse code's syntax and style for errors and vulnerabilities. Also, because ruff is written in Rust, it's fast. Some people are upset that this is a Python tool not written in Python, but this seems silly to me, given how many tools for non-Python things are written in Python (like Conan, for instance). So, that said, running ruff check is going to produce output that can help you find problems in your old Python2 code, like below:

Once you've fixed the issues found by ruff check, ruff format can be used to apply a consistent format to all of your source files and fix your whitespace/newline fetish (forgive me, I was young). For example (showing the whitespace as dots):

Old: in 2014 I loved the spaces!
New: so much neater in 2025.

I'd probably also remove the newline before the else statement (ruff doesn't care either way), but this is much improved and costs only a single command's worth of effort to format the entire code base. Unfortunately, ruff still leaves some ambiguity, such as when using the magic comma to avoid (I believe) its nastiest formatting output, so it would be nice if this could be enshrined in the configuration to prevent potential style based arguments.

Type Checkers (and Code Editors)

Perhaps inspired by Typescript, 10 years ago "Type Hints" were introduced to Python in PEP 484 (contributed to by my friend Mark Shannon). This allowed a user to add "hints" to source code regarding the types of arguments and variables and, frankly, it has revolutionized Python coding. What's incredible about type hints is they have no effect at runtime, but they allow analysis tools to reason about the correctness of code before it is executed. These tools are also smart enough to reason about a lot of code without any type hints being defined, such as you might find in a legacy Python2 code base (handy!).

Now, for all that ruff can do, it is not a type checker. Which brings me to another segue on a great advancement for Python programming since DTOcean was first written, which is modern code editors. Or one code editor in particular, Visual Studio Code. Previously, I was a long term Spyder user for Python development, but the extensibility and flexibility of VS Code, makes it my current favourite editor. The official VS Code Python extension comes with the pyright type checker built in, and this can be used to diagnose typing problems directly in the editor, such as shown below.

A typing error reported by pylance/pyright

The extension will highlight all typing problems detected for the currently open file. This is particularly useful for catching interface changes, from sources such as imported modules. Only one file at a time can be analysed in the editor, but we can also run pyright from the command line to check all the files in a project.

But You Still Need Runtime Testing

Unfortunately, it's still not possible for ruff and pyright to detect all the issues in source code without executing the code. Particularly, without including type hints pyright cannot determine the types of function arguments, and therefore API changes in those arguments will go uncaught until the code is run. Thus, having a comprehensive test suite remains as important as it always was.

In fact, it struck me that this might be the one time when having 100% code coverage (the percentage of the total lines of source code run during testing), regardless of test quality, would be useful. At least every line of code we are upgrading would be exercised. I have dismissed this terrible premise, though, as beyond the very limited use case of this particular exercise; such tests, that do not test the functionality of the code, are useless to harmful.

Runtime testing seems to be one area of Python development where tooling has not changed a great deal since 2014. The unit tests I wrote then were written using the pytest package, and it's still one of the most popular Python testing frameworks. Running a comprehensive test suite allows any bugs remaining after the static analysis to be caught. We can also use test failures to begin adding type hints at places where they are most useful. For instance, I had some function arguments which were classes with dictionary attributes, and so were ignored by pyright. The old dictionary "iter" methods caused a lot of issues to not be detected until I added type hints to the affected arguments.

Problematically for me, the unit test coverage of the DTOcean modules can be as low as 10%. Fortunately, there are a number of integration tests that were created (as is typical for code created by scientists), so I will still be able to get some diagnostic information, even if it's less efficient than a good unit-test suite. Work is ongoing to improve the unit-test coverage, but a great deal of refactoring is required for some of the modules.

Follow Along

In 2021, I thought upgrading DTOcean to Python3 was a task too daunting to consider. Yet, with modern tools at my disposal, it's been less difficult than I imagined. Other hurdles still exist, some of which I will cover in the next article on packaging, but I'm not worried about updating the source code, any more. If you'd like to keep tabs on how I am progressing with this upgrade, then do check out the next branch of the dtocean repository. And if you like what you see there, or want to see more content describing the journey, then I'd be super grateful if you'd consider sponsoring me through my GitHub Sponsors page. Thanks for reading.

To view or add a comment, sign in

Others also viewed

Explore topics