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_literalsThoroughly 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
andzip
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.