Skip to content

Instantly share code, notes, and snippets.

@rowillia
Last active December 7, 2016 21:45
Show Gist options
  • Save rowillia/c0feed97c1863b2d8e5a3ed73712df65 to your computer and use it in GitHub Desktop.
Save rowillia/c0feed97c1863b2d8e5a3ed73712df65 to your computer and use it in GitHub Desktop.
The Magic of the -3 Flag in Python 2

The Magic of the -3 Flag in Python 2

Porting code from Python 2 to Python 3 can be a daunting task. Tools like Futureize or Modernize can do most of the mechanical work for you, and Pylint can find obvious problems with code that's meant to be 2and3 compatible. You should absolutely be using these tools as they identify the lion's share of compatibility problems. Thanks to this work, it's really never been easier to port a large codebase to Python 3.

Even with these tools, however, porting code in a way that ensures identical behavior in Python 2 and Python 3 is tough. Python is a highly dynamic language and there is a huge breadth of changes between Python 2 and Python 3. Also, while we'd all love to work in code bases with 100% unit test coverage, the reality is unfortunately often very different. Given this, it's hard if not impossible for a static analysis tool to find potential all pitfalls, some of which might not cause unit tests to fail.

But there is hope! Buried deep in @brettcannon's excellent Porting Python 2 code to Python 3 document there's a brief reference to Python 2's -3 flag. The -3 flag will raise a DeprecationWarning at runtime if CPython encounters behavior that has either been removed or significantly changed in Python 3. You can use this in tests to flag potential problems, or even enable it in production to find potential problems with code that might not be well tested.

For example:

$ python -3
Python 2.7.12 (default, Jun 29 2016, 14:05:02)
[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.31)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 3/2
__main__:1: DeprecationWarning: classic int division
1
>>>

The above example is slightly contrived and caught by all of the static tools mentioned above. But, of course it's easy to come up with an example that will sail through these static tools:

$ python -3
Python 2.7.12 (default, Jun 29 2016, 14:05:02)
[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.31)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> number_value = 42
>>> number_value.__div__(12)
__main__:1: DeprecationWarning: classic int division
3
>>>

Great! Python still identifies this code as problematic in Python 3. This helps highlight the real power of enabling checking at runtime. By having Python itself flag potential problems we get much higher assurance our code will work as is in Python 3.

As a concrete example, I've been using this during test runs, and it's helped to find some potential problems with very popular third party libraries like simplejson, mock, coveragepy, or pytest as well as my own code. Some of these issues were actually OK, but it was better to remove the incompatible behavior anyway to help clean up the output of the -3 flag.

Most commonly, I've been finding problems with classes declaring a __eq__ method but not a __hash__ method. In Python 2, if a class were to do this it would get object.__hash__ as it's default hash method, which is likely never what you actually want. For example:

class JustEq(object):
   def __init__(self, x):
     self.x = x

   def __eq__(self, other):
     return self.x == other.x

list_of_things = [JustEq(1), JustEq(2), JustEq(3)]
set_of_things = set(list_of_things)  # Fails in Python 3

print(JustEq(1) in list_of_things)  # Prints 'True'
print(JustEq(1) in set_of_things)  # Prints 'False'.  Wat.

In Python 3, classes declaring __eq__ but not __hash__ get None as their __hash__ implementation and are thus unhashable. The above code will error out when converting list_of_things into a set. This means users of your code will see different behavior in Python 2 vs Python 3. I've since submitted a Pull Request to Pylint to flag this problem which should make it into their next release.

Incorporating the magic of -3 into your workflow

By default, these messages are just printed to stderr. If you're following the Twelve-Factor App methodology you can enable this flag in your production services and then you should then be able to track occurrences of these errors in your log-indexing service. These warnings can also be controlled via the warnings module or the -W command line flag to turn these warnings into runtime exceptions.

As noted above, there's a number of popular third party libraries that do not run cleanly under the -3 flag so you won't be able to use it everywhere (yet 😄). And while you can and should submit pull requests to fix those issues, to start getting value right now out of the -3 flag you can incrementally use the warnings module.

If you use pytest, you can trivially enable these warnings to fire during your test cases using a conftest.py file:

# conftest.py

import warnings
import sys

PY3K_WARNINGS = getattr(sys, 'py3kwarning', False)

if PY3K_WARNINGS:
    def pytest_runtest_setup(item):
        warnings.filterwarnings('error',
                                category=DeprecationWarning,
                                module='YOUR_MODULE_HERE')

    def pytest_runtest_teardown(item):
        warnings.filterwarnings('ignore', category=DeprecationWarning)

Then, instead of invoking py.test on the command line, invoke Python with the pytest module:

python -3 -m pytest ...

This will flag code with your code as it runs, but won't necessarily flag bad class declarations. Once you've got your code running cleanly, you can then remove the conftest.py file and instead use the -W flag:

python -3 -Werror::DeprecationWarning:YOUR_MODULE_HERE -m pytest ...

A patch has been submitted to Python to enable this flag via an environment variable instead of as a command line flag. This will be hugely useful to enable these warnings when Python is spawned by another process (like Gunicorn or Pytest x-dist).

Onward!

Python 3 is still happening. Python 2.8 isn't (or at least won't come from the core developers). The -3 flag gives developers a way to move past compatibility concerns by validating your code as it's running. Give it a try!

Copy link

ghost commented Oct 1, 2016

-PY3K_WARNINGS = sys.__dict__.get('py3kwarning')
-
-if PY3K_WARNINGS:
+if hasattr(sys,'py3kwarning'):

should work fine, havn't tested tho

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment