Migrating to Python 3

Python 2 is now EOL as of January 1st 2020. With the release of 1.4.0 OctoPrint will be compatible to both Python 2 and Python 3.

However, the same doesn’t automatically hold true for all of the third party plugins for OctoPrint out there - it will fall to their authors to ensure compatibility to both Python versions.

This guide is supposed to help plugin authors in making sure their plugins run under Python 2 as well as Python 3, which for now is the goal for OctoPrint’s ecosystem, as we’ll have to live with existing legacy Python 2 installations for a while to come (the plan is to stay Python 2 compatible until roughly a year after the release of 1.4.0).

How to get a Python 3 virtual environment with OctoPrint

In order to test your plugins for Python 3 compatibility and also to allow for ongoing maintenance against both Python versions, you should create a Python 3 virtual environment next to your Python 2 one. You can then quickly switch between Python 2 and Python 3 simply by activate-ing whichever one you need.

You can create a Python 3 virtualenv next to your (existing) Python 2 virtualenv and then just activate which one you currently want to use.

After installing Python 3 on your development system it’s as easy as supplying --python=/path/to/python3executable to virtualenv, e.g.:

virtualenv --python=/usr/bin/python3 venv3

That will have the virtualenv be created based on Python 3, regardless of whether it’s currently running under Python 2 or 3. The same works for Python 2 btw:

virtualenv --python=/usr/bin/python2 venv2

After creating the virtual environment, make sure to activate & install OctoPrint into it:

source venv3/bin/activate
pip install "OctoPrint>=1.4.0rc1"

Then create an editable install of your plugin, start the server and start testing:

pip install -e path/to/your/plugin
octoprint serve --debug

Note

On Windows that will probably look something like this instead:

virtualenv --python=C:/Python37/python.exe venv37
venv3/Script/activate.bat
pip install "OctoPrint>=1.4.0rc1"
pip install -e path/to/your/plugin
octoprint serve --debug

Note

If you want to migrate your existing OctoPrint install on OctoPi 0.17.0 to Python 3, I suggest to first make a backup, then move the existing venv /home/pi/oprint out of the way and create a new one based on Python 3 (which should already be present on current OctoPi images):

mv ~/oprint ~/oprint.py2
virtualenv --python=/usr/bin/python3 oprint
source ~/oprint/bin/activate
pip install "OctoPrint>=1.4.0"
sudo service octoprint restart

Telling OctoPrint your plugin is Python 3 ready

In order for OctoPrint to even load your plugin when it’s running under Python 3, it first needs to know your plugin is compatible to a Python 3 environment. By default OctoPrint will assume your plugin isn’t and refuse to load it when running under Python 3 itself.

To tell OctoPrint about this, all you need is to set the __plugin_pythoncompat__ property in your plugins’s __init__.py accordingly, e.g.

__plugin_pythoncompat__ = ">=2.7,<4"

This would tell OctoPrint that your plugin is compatible to all Python versions between 2.7 and 3.x. This should be your target compatibility range for now.

If at a later date you want to go all-in on Python 3 and mark your plugin as no longer supporting Python 2, tell OctoPrint about this as well:

__plugin_pythoncompat__ = ">=3,<4"

Note

You can also tell OctoPrint to ignore the Python compatibility flags for a specific plugin via config.yaml:

plugins:
  _forcedCompatible:
  - "myplugin"
  - "anotherplugin"

Note that this should only be used temporarily during testing and migration, or to mark an important plugin not under your own control that actually works fine under Python 3 out of the box as compatible while waiting until the plugin author has pushed an update including the needed flags. Do not just blindly mark third party plugins as compatible and then open support requests if that causes issues in your setup.

Once your plugin is ensured to be compatible and you’ve released a new version that includes the necessary compatibility flag and changes, is done you also need to mark up your plugin in the Official Plugin Repository (if it’s registered therein) so that OctoPrint’s built-in Plugin Manager will see that your plugin is compatible as well and allow users to install it through it. In order to do that, you need to add a new flag compatibility.python to the front matter in your plugin registration file and file a pull request for that. Adjust the markdown file so that it contains this:

compatibility:
  python: ">=2.7,<3"

The value here follows the same mechanism as the __plugin_pythoncompat__ property, so >=2.7,<3 for 2 and 3 support and >=3,<4 for 3+ support.

Warning

Do not just mark your plugin as compatible without diligent testing that it actually does work as expected and without flooding octoprint.log with warnings and errors!

Common pitfalls during migration

Some of the changes in Python 3 compared to Python 2 are sadly backwards incompatible and usually cause a number of common issues in code written for Python 2 when run under Python 3. By now they are pretty well documented and there exist a number of helpful and comprehensive migration guides, three of which I want to mention here.

One is the official Python 3 porting guide Porting Python 2 Code to Python 3 which sums up all the important changes and also gives hints on how best to go about running a project which supports both versions for now.

The second is the Writing Python 2-3 compatible code cheat sheet from the Python-Future project, which is a comprehensive list of idioms that are compatible to both Python 2 and 3 and will make your code run under both, utilizing future and six. I can strongly recommend this cheat sheet, it’s what primarily guided me during the migration phase as well.

The third one is the free online book Support Python 3: An in-depth guide, and especially its chapter on Common migration problems in which you’ll find extensive descriptions of the most troublesome changes in Python 3 and how to overcome them. Please note that with regards to the contents of this book, we are aiming for the “Python 2 and Python 3 without conversion” strategy, so code that runs in both environments. Sadly this book is a bit outdated by now and still references some long-out versions as “upcoming”, so with regards to compatible idioms to use, best stick to the Python-Future cheat sheet.

Looking at the issues encountered by some plugin authors and also my own experiences during the Python 3 migration of OctoPrint’s code, the most common problems for these scenarios seem to be byte vs unicode issues, trouble with absolute imports, changes in integer division behaviour and the switch of map, filter and zip to return iterators instead of lists and causing issues in the following code due to that.

Bytes vs unicode

One of if not the most problematic change between Python 2 and 3 surely must be the change in string handling. Under Python 2 your basic string was a byte string, but it could also magically turn into a unicode string depending on what you wrote into it. That did cause some confusion, especially in APIs, and caused quite a mess, which is why the decision was made to go for distinct text and binary types instead, and making the string literal always be a (unicode) text.

Note

Please note that these changes in string handling also affect several Python APIs that operate on files and streams and thus might also affect parts of OctoPrint’s plugin interface that inherit from these APIs. Currently only one such case has been reported, as OctoPrint’s LineProcessorStream will return bytes instead of str on its process_line function under Python 3 - so here’s a heads-up if your plugin happens to utilize that.

Obviously, that will lead to issues in code using “just strings” when run under Python 2 vs 3. The first step to solve these problems would be to make your scripts behave the same under Python 2 and 3 by putting this right at the top of all your plugin’s python files:

from __future__ import unicode_literals

That will make your files behave as if they were running under Python 3, even when run under Python 2, and your string literals will now be the text data type, which - annoyingly - is a different one under Python 2 vs 3, unicode vs str to be exact. Heads-up here - under Python 2 there’s also a str type, but that one is for binary data. Yes, I know, this ain’t fun.

In any case, once you’ve done this, make sure that everything in your code that should be text is text (unicode under Python 2, str under Python 3), and everything that should be binary is binary (str under Python 2, bytes under Python 3). A good rule of thumb is that you usually want to use text as much as possible within your application and only convert to/from bytes at the outskirts, e.g. when writing to a file, a socket or something else machine like. Note that you do NOT need to convert to bytes when implementing API endpoints that return JSON, as that should use text with unicode anyhow.

OctoPrint includes two utility methods you should use to ensure your strings enter/exit your code in the right format, under both Python versions: octoprint.util.to_bytes() and octoprint.util.to_unicode(). Use them to ensure the correct data types and to avoid weird conversion and encoding issues during runtime.

You can read more about this specific issue in the corresponding section of the Python porting guide and also in the cheat sheet.

Absolute imports

Python 3 now defaults to absolute imports, meaning that trying to import a sub package with a

import my_sub_package

will now fail with an error. You’ll need to explicitly make the import a relative one:

from . import my_sub_package

To make your code behave the same in that regard in both Python 2 and Python 3, you should add the corresponding future import:

from __future__ import absolute_imports

You can read more about this specific issue in the cheat sheet and also in the book.

Version specific imports

Sometimes it is necessary to use an import statement that is explicitly related to a specific Python version, e.g. due to a package change between Python 2 and 3. You can do this by first trying the Python 3 import and if that doesn’t work out trying the Python 2 import instead:

try:
   import queue
except ImportError:
   import Queue as queue

This should be the preferred method of handling situations like this. If you actually do need to do explicit version specific imports that cannot be handled this way, you can check for the Python version like this:

import sys
if sys.version[0] == '2':
   # Python 2 specific imports
else:
   # Python 3 specific imports

Integer division

When you divide two integers in Python 2 you’ll get back an integer, rounded down. In Python 3 however you’ll now get a float. That means you might have to revisit some places where you do integer divisions and might rely on the result to be an integer as well (e.g. when using a calculation result as an index in an array or something like that).

Yet again there’s a future-import to apply to your files in order to at least have them behave the same in that regard under both Python 2 and Python 3:

from __future__ import division

You can read more about this specific issue in the Python porting guide and in the cheat sheet.

Iterators instead of list from map, filter, zip

The built-in functions map, filter and zip return a list with their result in Python 2. In Python 3 they have been switched to returning iterators. That can cause trouble with code handling the result (e.g. if you try to return it as part of a JSON response on an API endpoint).

The easiest way to solve this is to make sure to wrap any map/filter/zip calls into a list constructor if the result is to be used outside of the calling code (even though that comes with a small performance penalty under Python 2):

result1 = filter(lambda x: x is not None, my_collection)
result2 = list(filter(lambda x: x is not None, my_collection))

assert(isinstance(result1, list)) # Python 2 passes, Python 3 fails
assert(isinstance(result2, list)) # Python 2 and 3 pass

There also exist further options, take a look at the cheat sheet.

Checklist

As a summary, follow this checklist to migrate your plugin to be compatible to both Python 2 and 3:

  • Create a Python 3 virtualenv and install OctoPrint and your plugin into it for testing.

  • Tell OctoPrint your plugin is Python 2 and 3 compatible by adding a new property __plugin_pycompat__ to its __init__.py:

    __plugin_pythoncompat__ = ">=2.7,<4"
    
  • Add a compatibility header to all py files to ensure similar basic behaviour under Python 2 and Python 3:

    # -*- coding: utf-8 -*-
    from __future__ import absolute_import, division, print_function, unicode_literals
    
  • Thoroughly test your plugin under Python 3. Pay special attention to any kind of string handling issues, integer division, relative imports from your plugin package and how the results of map, filter and zip are used in your code, as those have proven to be the biggest issues during past migrations.

  • Once everything works under both Python versions and you’ve prepared a new release of your plugin (don’t forget to increment the version!), update your registration file in the Official Plugin Repository to include the correct Python compatibility information as well:

    compatibility:
      python: ">=2.7,<4"
    

Further reading

See also

Porting Python 2 Code to Python 3

The official Python 3 porting guide which sums up all the important changes and also gives hints on how best to go about running a project which supports both versions for now.

Cheat Sheet: Writing Python 2-3 compatible code

A comprehensive list of idioms that are compatible to both Python 2 and 3 and will make your code run under both, utilizing future and six. Strongly recommended.

Supporting Python 3: An in-depth guide

A free online book on the switch to Python 3. Sadly seems a bit outdated by now, so with regards to compatible idioms to use, best stick to the cheat sheet. Gives some interesting background however.

Towards Python 3 and OctoPrint 1.4.0

Forum topic discussing OctoPrint 1.4.0’s roadmap including Python 3 compatibility and time frame.

Migrating plugins to Python 2 & 3 compatibility - experiences?

Forum topic collecting experiences by plugin developers in migrating their plugins to achieve Python 2 & 3 compatibility.