Python – Strategy for Keeping Up with Language Changes

python

Writing code that will still run years from now

Programming languages change.
Libraries change.
Some code from 5, 10, or even 20 years ago might still run and produce expected results, whereas some code from 2 years might fail with a syntax error.
This is partly inevitable, since languages evolve (at least, most do).
Developers have a responsibility to maintain their code. But sometimes, stability is an important requirement in production code, and code should simply run for 10 years without the need for someone going through the code every year to adapt it for language changes.
Or I might have small scripts, for example for scientific data analysis, that I need to revisit after not touching them for years.
For example, at meteorological offices there is a lot of operational Fortran code even for non-speed-essential parts, and code stability is one of the reasons.
I've heard fear for instability is one of the objects they have against moving to Python (apart from language inertia of course; it's only possible for new code not dependent on old code).
Of course, one strategy for stable code is to freeze the entire operating system. But that is not always feasible.

I'm using Python as on example, but the issue is not limited to Python in particular.

Documents on Python compatibility issues

In the case of Python, there are several documents outlining policy for backward-incompatible changes.

PEP-5

According to PEP 5:

There must be at least a one-year transition period between the
release of the transitional version of Python and the release
of the backwards incompatible version. Users will have at
least a year to test their programs and migrate them from use
of the deprecated construct to the alternative one.

Personally, I consider that one year is rather short. It means I might write some code, and 1½ years from now it won't run anymore.

PEP 291

PEP 291 contains an incomplete lists of guidelines of things that should be avoided in order to maintain backward compatibility. However, it relates only to Python 2.x. As Python 2.7 is the final release in the 2.x series and Python 2.7 is bugfix-only, this PEP is now only of historical interest.

PEP 387

There is also PEP 387 on backward-incompatible changes. PEP 387 is a draft and not official policy. In June 2009, this was discussed on the Python-ideas mailing-list. Part of the discussion focussed on how developers can write code that is robust against language changes. One post listed some advice on what not to do:

Along with this there are several rules you can infer that are probably
true most of the time: don't call stuff starting with "_", don't monkey-
patch anything, don't use dynamic class replacement on objects from
classes other than your own, don't depend on the depth of inheritance
hierarchies (for example, no ".__bases__[0].__bases__[0]"), make sure
your tests run without producing any DeprecationWarnings, be mindful of
potential namespace conflicts when adding attributes to classes that
inherit from other libraries. I don't think all these things are
written down in one place though.

In addition, there were some points about "mine fields" (new features likely to change) and "frozen areas" (very sold APIs virtually guaranteed not to change). Quoting Antoine Pitrou:

I think the "frozen area" should be defined positively (explicit public APIs and
explicitly guaranteed behaviour) rather than negatively (an explicit "mine
field"). Otherwise, we will forget to put some important things in the minefield
and get bitten later when we need to change those things in a
backwards-incompatible way.

There doesn't seem to be any conclusion from this thread, but it gets pretty close to the core of what I'm looking for. The thread is almost four years old, so perhaps the situation has changed or improved. What kind of code is likely to survive, and what kind of code is more fragile?

Porting guidelines

In addition to the documents outlined above, each Python version comes with a porting guideline: porting to Python 3.2, porting to Python 3.3, etc.

Useful compatibility

PEP 3151 introduced me to the concept of useful compatibility. In my own words, this boils down to the idea that only if code is carefully written language developers need to be careful to maintain compatibility. It doesn't really define useful compatibility, but I think it's similar to the ideas I quoted from the PEP 387 discussion above.

From the programmers' point of view

As a programmer, I know that Python will change in the future and that people — most notably myself — will try to run my code perhaps several years from now in a Python version that is one, two, or perhaps three minor versions up. Not everything will be compatible, and in fact it's easy to come up with code that will fail (I once encountered code stating if sys.version[:3] != '2.3': print 'Wrong version, exiting'). What I'm looking for is a set of guidelines on what to do and what not to do to enhance the chances that my code will still run unaltered in the future.

Are there any such guidelines? How do I write Python code that will still run in the future?

My question relates both to the Python core, to its standard library, but also to commonly used add-on libraries, in particular numpy, scipy, matplotlib.


EDIT: So far, two of the answers relate to python2 vs. python3. This is not what I mean. I know about tools to migrate from Python2 to Python3. My question relates to language changes yet to come. We can do better than a crystal ball in finding coding guidelines that are more stable. For example:

  • import module is more future-proof than from module import *, because the latter can break code if module grows one or more new functions/classes.

  • Using undocumented methods may be less future-proof than using documented methods, as something being undocumented may be a sign of something being not stable yet.

It's this kind of practical coding advices that I'm after. Since it's about present→future, we can limit ourselves to Python3, because Python2 is not going to change anymore.

Best Answer

This is an unsolved problem in our field. There's no way to be sure that your code will indefinitely work. Even if your code was truly perfect in the forwards-compatible sense (and if it is, please come work for my company! ;) ), if it runs on, uses, or is used by any other software which gets a bug or changes in any way, your code may not work.

So I can't give you a list of things to do that, if you follow them, will guarantee success. But what you can do is minimize the risk of future breakages and minimize their impacts. A more knowledgeable Pythonist would be able to give you advice more specific to Python, so I will have to be more general:

  • write unit tests. Even for things that you know don't need them.

  • using popular/well-designed/stable libraries and technologies, avoiding unpopular (and thus likely to soon be unsupported) ones

  • avoid writing code that exploits implementation details. Code to interfaces, not implementations. Code against multiple implementations of the same interface. For example, run your code in CPython, Jython, and IronPython and see what happens. This will give you some great feedback about your code. This might not be helpful for Python3 though -- last I heard, some implementations were still in Python2.

  • write simple, clear code that is explicit about its assumptions

  • write modular, composable code. If some code must do something dangerous (in the future-proof sense), separate it so that even if it has to change, the rest of the code doesn't.

  • have a specification of some form. This is similar to the points about unit tests, if you use tests as a spec, and interfaces, which can also be used as specs. (I mean interface in the general sense, not the Java keyword sense).

Doing any of these things may/will increase the amount of work you have to do. I think that makes sense -- a lot of these points can also be made for how to write good code, which is quite difficult (in my opinion). Sometimes you may need to violate some of these suggestions. That is perfectly acceptable, but be aware of the costs.

It's great that the Python team is thinking about this, and for sure they are far more talented and skilled than I will ever be. Still, I would estimate there's a 100% that somebody's code somewhere will stop working the way the want it's intended to when Python is upgraded.