Nota

¡Ayúdanos a traducir la documentación oficial de Python al Español! Puedes encontrar más información en Como contribuir. Ayuda a acercar Python a más personas de habla hispana.

How to port Python 2 Code to Python 3

autor:

Brett Cannon

La breve explicación

To achieve Python 2/3 compatibility in a single code base, the basic steps are:

  1. Sólo preocúpate por admitir Python 2.7

  2. Asegúrese de tener una buena cobertura de prueba (coberturas.py puede ayudar; python -m pip install coverage)

  3. Learn the differences between Python 2 and 3

  4. Utilice Futurize (o Modernize) para actualizar su código (por ejemplo, python -m pip install future)

  5. Use Pylint para asegurarse de que no retrocede en su compatibilidad con Python 3 (python -m pip install pylint)

  6. Use caniusepython3 para averiguar cuáles de sus dependencias están bloqueando el uso de Python 3 (python -m pip install caniusepython3)

  7. Once your dependencies are no longer blocking you, use continuous integration to make sure you stay compatible with Python 2 and 3 (tox can help test against multiple versions of Python; python -m pip install tox)

  8. Consider using optional static type checking to make sure your type usage works in both Python 2 and 3 (e.g. use mypy to check your typing under both Python 2 and Python 3; python -m pip install mypy).

Nota

Nota: El uso de python -m pip install garantiza que el pip que invoca es el que está instalado para el Python actualmente en uso, ya sea un pip de todo el sistema o uno instalado dentro de un entorno virtual.

Detalles

Even if other factors - say, dependencies over which you have no control - still require you to support Python 2, that does not prevent you taking the step of including Python 3 support.

Most changes required to support Python 3 lead to cleaner code using newer practices even in Python 2 code.

Different versions of Python 2

Ideally, your code should be compatible with Python 2.7, which was the last supported version of Python 2.

Some of the tools mentioned in this guide will not work with Python 2.6.

If absolutely necessary, the six project can help you support Python 2.5 and 3 simultaneously. Do realize, though, that nearly all the projects listed in this guide will not be available to you.

If you are able to skip Python 2.5 and older, the required changes to your code will be minimal. At worst you will have to use a function instead of a method in some instances or have to import a function instead of using a built-in one.

Asegúrese de especificar el soporte de versión adecuado en su archivo setup.py

En su archivo setup.py debe tener el trove classifier adecuado especificando qué versiones de Python admite. Como su proyecto no es compatible con Python 3, al menos debe tener Programming Language :: Python :: 2 :: Only especificado. Idealmente también debe especificar cada versión principal/menor de Python que admita, por ejemplo, Programming Language :: Python :: 2.7.

Tener una buena cobertura de prueba

Una vez que tenga su código compatible con la versión más antigua de Python 2 que desee, querrá asegurarse de que su conjunto de pruebas tenga una buena cobertura. Una buena regla general es que si desea tener la suficiente confianza en su conjunto de pruebas, cualquier falla que aparezca después de que las herramientas reescriban su código son errores reales en las herramientas y no en su código. Si desea un número al que apuntar, intente obtener una cobertura superior al 80% (y no se sienta mal si le resulta difícil obtener una cobertura superior al 90%). Si aún no tiene una herramienta para medir la cobertura de la prueba, se recomienda cover.py.

Be aware of the differences between Python 2 and 3

Once you have your code well-tested you are ready to begin porting your code to Python 3! But to fully understand how your code is going to change and what you want to look out for while you code, you will want to learn what changes Python 3 makes in terms of Python 2.

Some resources for understanding the differences and their implications for you code:

Actualiza tu código

There are tools available that can port your code automatically.

Futurize does its best to make Python 3 idioms and practices exist in Python 2, e.g. backporting the bytes type from Python 3 so that you have semantic parity between the major versions of Python. This is the better approach for most cases.

Modernize, on the other hand, is more conservative and targets a Python 2/3 subset of Python, directly relying on six to help provide compatibility.

A good approach is to run the tool over your test suite first and visually inspect the diff to make sure the transformation is accurate. After you have transformed your test suite and verified that all the tests still pass as expected, then you can transform your application code knowing that any tests which fail is a translation failure.

Unfortunately the tools can’t automate everything to make your code work under Python 3, and you will also need to read the tools” documentation in case some options you need are turned off by default.

Key issues to be aware of and check for:

División

In Python 3, 5 / 2 == 2.5 and not 2 as it was in Python 2; all division between int values result in a float. This change has actually been planned since Python 2.2 which was released in 2002. Since then users have been encouraged to add from __future__ import division to any and all files which use the / and // operators or to be running the interpreter with the -Q flag. If you have not been doing this then you will need to go through your code and do two things:

  1. Añadir from __future__ import division a sus archivos

  2. Actualice cualquier operador de división según sea necesario para utilizar // para usar la división entera a la baja o continuar usando / y esperar un número flotante

La razón por la que / no se traduce simplemente a // automáticamente es que si un objeto define un método __truediv__ pero no __floordiv__ entonces su código comenzaría a fallar (por ejemplo, una clase definida por el usuario que utiliza / para significar alguna operación pero no // para la misma cosa o en absoluto).

Texto frente a datos binarios

En Python 2 puede usar el tipo str tanto para texto como para datos binarios. Desafortunadamente, esta confluencia de dos conceptos diferentes podría conducir a código frágil que a veces funcionaba para cualquier tipo de datos, a veces no. También podría dar lugar a API confusas si las personas no declaraban explícitamente que algo que aceptaba str aceptaba datos binarios o de texto en lugar de un tipo específico. Esto complicó la situación especialmente para cualquier persona que admita varios idiomas, ya que las API no se molestarían explícitamente en admitir explícitamente Unicode cuando reclamaban compatibilidad con datos de texto.

Python 3 made text and binary data distinct types that cannot simply be mixed together. For any code that deals only with text or only binary data, this separation doesn’t pose an issue. But for code that has to deal with both, it does mean you might have to now care about when you are using text compared to binary data, which is why this cannot be entirely automated.

Decide which APIs take text and which take binary (it is highly recommended you don’t design APIs that can take both due to the difficulty of keeping the code working; as stated earlier it is difficult to do well). In Python 2 this means making sure the APIs that take text can work with unicode and those that work with binary data work with the bytes type from Python 3 (which is a subset of str in Python 2 and acts as an alias for bytes type in Python 2). Usually the biggest issue is realizing which methods exist on which types in Python 2 and 3 simultaneously (for text that’s unicode in Python 2 and str in Python 3, for binary that’s str/bytes in Python 2 and bytes in Python 3).

The following table lists the unique methods of each data type across Python 2 and 3 (e.g., the decode() method is usable on the equivalent binary data type in either Python 2 or 3, but it can’t be used by the textual data type consistently between Python 2 and 3 because str in Python 3 doesn’t have the method). Do note that as of Python 3.5 the __mod__ method was added to the bytes type.

Datos de texto

Datos binarios

decode

encode

format

isdecimal

isnumeric

La creación de la distinción más fácil de controlar se puede realizar mediante la codificación y descodificación entre datos binarios y texto en el borde del código. Esto significa que cuando reciba texto en datos binarios, debe descodificarlo inmediatamente. Y si el código necesita enviar texto como datos binarios, codificarlo lo más tarde posible. Esto permite que el código funcione solo con texto internamente y, por lo tanto, elimina tener que realizar un seguimiento del tipo de datos con los que está trabajando.

The next issue is making sure you know whether the string literals in your code represent text or binary data. You should add a b prefix to any literal that presents binary data. For text you should add a u prefix to the text literal. (There is a __future__ import to force all unspecified literals to be Unicode, but usage has shown it isn’t as effective as adding a b or u prefix to all literals explicitly)

You also need to be careful about opening files. Possibly you have not always bothered to add the b mode when opening a binary file (e.g., rb for binary reading). Under Python 3, binary files and text files are clearly distinct and mutually incompatible; see the io module for details. Therefore, you must make a decision of whether a file will be used for binary access (allowing binary data to be read and/or written) or textual access (allowing text data to be read and/or written). You should also use io.open() for opening files instead of the built-in open() function as the io module is consistent from Python 2 to 3 while the built-in open() function is not (in Python 3 it’s actually io.open()). Do not bother with the outdated practice of using codecs.open() as that’s only necessary for keeping compatibility with Python 2.5.

The constructors of both str and bytes have different semantics for the same arguments between Python 2 and 3. Passing an integer to bytes in Python 2 will give you the string representation of the integer: bytes(3) == '3'. But in Python 3, an integer argument to bytes will give you a bytes object as long as the integer specified, filled with null bytes: bytes(3) == b'\x00\x00\x00'. A similar worry is necessary when passing a bytes object to str. In Python 2 you just get the bytes object back: str(b'3') == b'3'. But in Python 3 you get the string representation of the bytes object: str(b'3') == "b'3'".

Por último, la indexación de datos binarios requiere un control cuidadoso (el corte no requiere ningún control especial). En Python 2, b'123'[1] == b'2' mientras que en Python 3 b'123'[1] == 50. Dado que los datos binarios son simplemente una colección de números binarios, Python 3 retorna el valor entero para el byte en el que indexa. Pero en Python 2, ya que bytes == str, la indexación retorna un segmento de bytes de un solo elemento. El proyecto six tiene una función denominada six.indexbytes() que devolverá un entero como en Python 3: six.indexbytes(b'123', 1).

Para resumir:

  1. Decida cuál de sus API toma texto y cuáles toman datos binarios

  2. Asegúrese de que el código que funciona con texto también funciona con unicode y el código para datos binarios funciona con bytes en Python 2 (consulte la tabla anterior para los métodos que no puede usar para cada tipo)

  3. Marque todos los literales binarios con un prefijo b, literales textuales con un prefijo u

  4. Descodificar datos binarios en texto tan pronto como sea posible, codificar texto como datos binarios tan tarde como sea posible

  5. Abra los archivos con io.open() y asegúrese de especificar el modo b cuando sea apropiado

  6. Tenga cuidado al indexar en datos binarios

Utilice la detección de funciones en lugar de la detección de versiones

Inevitablemente tendrá código que tiene que elegir qué hacer en función de qué versión de Python se está ejecutando. La mejor manera de hacerlo es con la detección de características de si la versión de Python en la que se ejecuta es compatible con lo que necesita. Si por alguna razón eso no funciona, entonces usted debe hacer que la comprobación de la versión sea contra Python 2 y no Python 3. Para ayudar a explicar esto, veamos un ejemplo.

Supongamos que necesita acceso a una característica de importlib que está disponible en la biblioteca estándar de Python desde Python 3.3 y disponible para Python 2 a través de importlib2 en PyPI. Es posible que tenga la tentación de escribir código para acceder, por ejemplo, al módulo importlib.abc haciendo lo siguiente:

import sys

if sys.version_info[0] == 3:
    from importlib import abc
else:
    from importlib2 import abc

El problema con este código es ¿qué sucede cuando sale Python 4? Sería mejor tratar Python 2 como el caso excepcional en lugar de Python 3 y asumir que las futuras versiones de Python serán más compatibles con Python 3 que Python 2:

import sys

if sys.version_info[0] > 2:
    from importlib import abc
else:
    from importlib2 import abc

La mejor solución, sin embargo, es no hacer ninguna detección de versiones en absoluto y en su lugar confiar en la detección de características. Esto evita cualquier problema potencial de conseguir la detección de la versión incorrecta y le ayuda a mantenerse compatible con el futuro:

try:
    from importlib import abc
except ImportError:
    from importlib2 import abc

Evitar regresiones de compatibilidad

Una vez que haya traducido completamente el código para que sea compatible con Python 3, querrá asegurarse de que el código no retroceda y deje de funcionar bajo Python 3. Esto es especialmente cierto si tiene una dependencia que le está bloqueando para que no se ejecute realmente en Python 3 en este momento.

Para ayudar a mantenerse compatible, los módulos nuevos que cree deben tener al menos el siguiente bloque de código en la parte superior del misma:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

También puede ejecutar Python 2 con el indicador -3 para recibir una advertencia sobre varios problemas de compatibilidad que el código desencadena durante la ejecución. Si convierte las advertencias en errores con -Werror, puede asegurarse de que no se pierda accidentalmente una advertencia.

También puede usar el proyecto de Pylint y su indicador --py3k para lintar el código para recibir advertencias cuando el código comienza a desviarse de la compatibilidad con Python 3. Esto también evita que tenga que ejecutar Modernize o Futurize sobre el código con regularidad para detectar las regresiones de compatibilidad. Esto requiere que solo admita Python 2.7 y Python 3.4 o posterior, ya que es la compatibilidad mínima de la versión mínima de Python de Pylint.

Compruebe qué dependencias bloquean la transición

Después de que haya hecho que su código sea compatible con Python 3, debe empezar a preocuparse por si sus dependencias también se han portado. El proyecto caniusepython3 se creó para ayudarle a determinar qué proyectos – directa o indirectamente – le impiden admitir Python 3. Hay una herramienta de línea de comandos, así como una interfaz web en https://caniusepython3.com.

El proyecto también proporciona código que puede integrar en el conjunto de pruebas para que tenga una prueba con errores cuando ya no tenga dependencias que le impidan usar Python 3. Esto le permite evitar tener que comprobar manualmente sus dependencias y recibir notificaciones rápidamente cuando puede empezar a ejecutarse en Python 3.

Actualice su archivo setup.py para denotar compatibilidad con Python 3

Una vez que el código funciona en Python 3, debe actualizar los clasificadores en su setup.py para que contenga Programming Language :: Python :: 3 y no especificar solo compatibilidad con Python 2. Esto le dirá a cualquier persona que use su código que admite Python 2 y 3. Lo ideal es que también desee agregar clasificadores para cada versión principal/menor de Python que ahora admita.

Utilice la integración continua para seguir siendo compatible

Once you are able to fully run under Python 3 you will want to make sure your code always works under both Python 2 and 3. Probably the best tool for running your tests under multiple Python interpreters is tox. You can then integrate tox with your continuous integration system so that you never accidentally break Python 2 or 3 support.

También es posible que desee utilizar el indicador -bb con el intérprete de Python 3 para desencadenar una excepción cuando se comparan bytes con cadenas o bytes con un int (este último está disponible a partir de Python 3.5). De forma predeterminada, las comparaciones de tipos diferentes simplemente retornan False, pero si cometió un error en la separación del control de datos de texto/binario o la indexación en bytes, no encontraría fácilmente el error. Esta marca lanzará una excepción cuando se produzcan este tipo de comparaciones, lo que hace que el error sea mucho más fácil de rastrear.

Considere la posibilidad de usar la comprobación de tipos estáticos opcionales

Otra forma de ayudar a transferir el código es usar un comprobador de tipos estáticos como mypy o pytype en el código. Estas herramientas se pueden utilizar para analizar el código como si se estuviera ejecutando en Python 2, puede ejecutar la herramienta por segunda vez como si el código se ejecutara en Python 3. Al ejecutar un comprobador de tipos estáticos dos veces como este, puede descubrir si, por ejemplo, está usando incorrectamente el tipo de datos binarios en una versión de Python en comparación con otra. Si agrega sugerencias de tipo opcionales al código, también puede indicar explícitamente si las API usan datos textuales o binarios, lo que ayuda a asegurarse de que todo funciona según lo esperado en ambas versiones de Python.