Passage à Python 3

Django 1.5 est la première version de Django à prendre en charge Python 3. Le même code fonctionne aussi bien avec Python 2 (≥ 2.6.5) qu’avec Python 3 (≥ 3.2), grâce à la couche de compatibilité offerte par six.

Ce document est principalement destiné aux rédacteurs d’applications réutilisables qui souhaitent prendre en charge à la fois Python 2 et 3. Il présente également des lignes de conduites adoptées dans le code de Django.

Philosophie

Ce document suppose que les changements entre Python 2 et Python 3 vous sont familiers. Dans le cas contraire, lisez d’abord Python’s official porting guide. Le rafraîchissement de vos connaissances de la gestion de l’unicode dans Python 2 et 3 peut être utile ; la présentation Pragmatic Unicode est une bonne ressource.

Django a choisi la stratégie source compatible Python 2 et 3. Vous êtes évidemment libre de choisir une autre stratégie pour votre propre code, particulièrement si vous n’avez pas besoin de rester compatible avec Python 2. Mais les auteurs d’applications enfichables sont encouragés à adopter la même stratégie de transition que Django.

L’écriture de code compatible est bien plus facile si vous ciblez Python ≥ 2.6. Django 1.5 introduit des outils de compatibilité tels que django.utils.six, qui est une version adaptée du module six. Par commodité, des alias de compatibilité ascendante ont été introduits dans Django 1.4.2. Si votre application tire profit de ces outils, elle nécessitera au moins Django ≥ 1.4.2.

De toute évidence, l’écriture de code source compatible ajoute une certaine charge qui peut parfois être frustrante. Les développeurs de Django ont constaté que d’écrire du code Python 3 en veillant à ce qu’il reste compatible avec Python 2 est plus gratifiant que l’inverse. Non seulement le code sera plus pérenne, mais les avantages de Python 3 (comme une gestion plus saine du texte) se révèlent assez rapidement. La contrainte de Python 2 devient une exigence de rétrocompatibilité et nous, développeurs, sommes habitués à faire face à de telles contraintes.

Les outils de migration fournis par Django sont inspirés de cette approche et cela se reflète tout au long de ce guide.

Aides à la migration

Chaînes littérales unicode

Cette étape consiste à :

  • Ajouter from __future__ import unicode_literals au début de chaque module Python. Il est conseillé de le mettre vraiment dans tous les modules, sinon vous ne saurez jamais dans quel mode vous êtes sans chaque fois regarder au début du fichier concerné.

  • Enlever les préfixes u devant toutes les chaînes unicode.

  • Ajouter un préfixe b devant les chaînes d’octets.

L’application systématique de ces modifications garantit la rétrocompatibilité.

Cependant, les applications Django ne nécessitent généralement pas de chaînes d’octets, car Django n’expose que des interfaces unicode au programmeur. Python 3 décourage l’utilisation de chaînes d’octets, sauf pour les données binaires ou les interfaces orientées octets. Dans Python 2, les chaînes d’octets et les chaînes unicode sont pratiquement interchangeables tant qu’elles ne contiennent pas de données non ASCII. Profitez de ceci pour employer des chaînes unicode autant que possible en évitant les préfixes b.

Note

Le préfixe u de Python 2 est une erreur de syntaxe dans Python 3.2, mais il est de nouveau autorisé dans Python 3 suite à la PEP 414. Ainsi, cette transformation est facultative si vous ciblez Python ≥ 3.3. Mais c’est toujours recommandé, selon la philosophie « écrivons du code Python 3 ».

Gestion des chaînes

Le type unicode de Python 2 a été renommé str dans Python 3, str() a été renommé bytes et basestring a disparu. six offre des outils pour s’occuper de ces modifications.

Django contient aussi plusieurs classes et fonctions liées aux chaînes dans les modules django.utils.encoding et django.utils.safestring. Leurs noms contenait le mot str, qui n’a pas la même signification dans Python 2 et dans Python 3, et unicode, qui n’existe plus dans Python 3. Afin d’éviter des ambiguïtés et de la confusion, ces concepts ont été renommés bytes et text.

Voici les changements de noms effectués dans django.utils.encoding:

Ancien nom

Nouveau nom

smart_str smart_bytes
smart_unicode smart_text
force_unicode force_text

Pour des motifs de rétrocompatibilité, les anciens noms fonctionnent encore avec Python 2. Avec Python 3, smart_str est un alias de smart_text.

Pour des motifs de compatibilité ascendante, les nouveaux noms sont valables dès Django 1.4.2.

Note

django.utils.encoding a été réécrit en profondeur dans Django 1.5 pour fournir une API plus cohérente. Consultez sa documentation pour plus d’informations.

django.utils.safestring est principalement utilisée par les fonctions mark_safe() et mark_for_escaping() qui n’ont elles-même pas changé. Dans le cas où vous utilisez ces structures internes, voici les modifications de noms effectuées :

Ancien nom

Nouveau nom

EscapeString EscapeBytes
EscapeUnicode EscapeText
SafeString SafeBytes
SafeUnicode SafeText

Pour des motifs de rétrocompatibilité, les anciens noms fonctionnent encore avec Python 2. Avec Python 3, EscapeString et SafeString sont respectivement des alias de EscapeText et SafeText.

Pour des motifs de compatibilité ascendante, les nouveaux noms sont valables dès Django 1.4.2.

Méthodes __str__() et __unicode__()

Dans Python 2, le modèle d’objet spécifie les méthodes __str__() et __unicode__(). Si ces méthodes existent, elles doivent renvoyer respectivement str (octets) et unicode (texte).

L’instruction print et la fonction native str appellent __str__() pour déterminer une représentation humainement lisible d’un objet. La fonction native unicode appelle __unicode__() si elle existe, et se rabat sinon sur __str__() et décode le résultat avec le codage système. Inversement, la classe de base Model déduit automatiquement __str__() à partir de __unicode__() en codant le résultat en UTF-8.

Avec Python 3, il n’existe que __str__() qui doit renvoyer du contenu str (texte).

(Il est aussi possible de définir __bytes__(), mais les applications Django n’ont que peu l’utilité de cette méthode, parce qu’elles ont rarement affaire directement à des octets (bytes).)

Django propose une manière simple de définir les méthodes __str__() et __unicode__() qui fonctionnent à la fois avec Python 2 et 3 : vous devez définir une méthode __str__() renvoyant du texte et en lui appliquant le décorateur python_2_unicode_compatible().

Avec Python 3, le décorateur effectue une opération blanche. Avec Python 2, il définit les méthodes __unicode__() et __str__() appropriées (remplaçant la méthode __str__() originale dans le processus). Voici un exemple :

from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible

@python_2_unicode_compatible
class MyClass(object):
    def __str__(self):
        return "Instance of my class"

Cette technique est le meilleur exemple de la philosophie de migration adoptée par Django.

Pour des motifs de compatibilité ascendante, ce décorateur est disponible dès Django 1.4.2.

Finalement, notez que __repr__() doit renvoyer du contenu de type str dans toutes les versions de Python.

Classes dict et classes affiliées

Les méthodes de dictionnaire dict.keys(), dict.items() et dict.values() renvoient des listes en Python 2 et des itérateurs en Python 3. Les classes QueryDict et autres classes de type dictionnaire définies dans django.utils.datastructures se comportent de la même manière en Python 3.

six fournit des fonctions de compatibilité pour prendre en compte cette modification : iterkeys(), iteritems() et itervalues(). Il contient également une fonction non documentée iterlists qui fonctionne bien pour django.utils.datastructures.MultiValueDict et ses sous-classes.

Objets HttpRequest et HttpResponse

Selon PEP 3333:

  • les en-têtes sont toujours des objets de type str,

  • les flux d’entrée et de sortie sont toujours des objets de type bytes (octets).

Plus précisément, HttpResponse.content contient des octets, ce qui peut poser problème quand on le compare avec un contenu str dans les tests. La solution privilégiée est de compter sur assertContains() et assertNotContains(). Ces méthodes acceptent une réponse et une chaîne unicode comme paramètres.

Lignes directrices pour le code

Les lignes directrices suivantes ont été appliquées dans le code source de Django. Elles sont aussi recommandées pour les applications tierces qui suivent la même stratégie de migration.

Contraintes de syntaxe

Unicode

En Python 3, toutes les chaînes sont considérées par défaut comme de l’unicode. Le type unicode de Python 2 est appelé str en Python 3 et l’ancien type str est devenu bytes.

Vous ne pouvez pas utiliser le préfixe u devant une chaîne littérale unicode parce que cela constitue une erreur de syntaxe en Python 3.2. Vous devez par contre préfixer les chaînes d’octets avec b.

Afin d’activer un comportement identique en Python 2, chaque module doit importer unicode_literals provenant de __future__:

from __future__ import unicode_literals

my_string = "This is an unicode literal"
my_bytestring = b"This is a bytestring"

Si vous avez besoin d’une chaîne littérale d’octets en Python 2 et que cette même chaîne littérale soit de l’unicode en Python 3, utilisez la fonction native str:

str('my string')

En Python 3, aucune conversion automatique n’est effectuée entre les types str et bytes, et le module codecs est devenu plus strict. str.encode() renvoie toujours du contenu de type bytes et bytes.decode du contenu de type str. Par conséquent, la structure suivante est parfois nécessaire :

value = value.encode('ascii', 'ignore').decode('ascii')

Soyez prudent si vous devez indexer des chaînes d’octets.

Exceptions

Lorsque vous interceptez des exceptions, utilisez le mot-clé as:

try:
    ...
except MyException as exc:
    ...

Cette ancienne syntaxe a été supprimée en Python 3 :

try:
    ...
except MyException, exc:    # Don't do that!
    ...

La syntaxe pour repropager une exception avec une pile d’appel différente a aussi été modifiée. Utilisez six.reraise().

Méthodes magiques

Utilisez les modèles ci-dessous pour prendre en charge les méthodes magiques renommées en Python 3.

Itérateurs

class MyIterator(six.Iterator):
    def __iter__(self):
        return self             # implement some logic here

    def __next__(self):
        raise StopIteration     # implement some logic here

Évaluation booléenne

class MyBoolean(object):

    def __bool__(self):
        return True             # implement some logic here

    def __nonzero__(self):      # Python 2 compatibility
        return type(self).__bool__(self)

Division

class MyDivisible(object):

    def __truediv__(self, other):
        return self / other     # implement some logic here

    def __div__(self, other):   # Python 2 compatibility
        return type(self).__truediv__(self, other)

    def __itruediv__(self, other):
        return self // other    # implement some logic here

    def __idiv__(self, other):  # Python 2 compatibility
        return type(self).__itruediv__(self, other)

Ces méthodes spéciales sont recherchées au niveau de la classe, et non de l’instance, pour refléter le comportement de l’interpréteur Python.

Écriture de code compatible avec six

six est la bibliothèque de compatibilité incontournable pour prendre en charge à la fois Python 2 et 3 dans une même base de code. Lisez sa documentation !

Une version adaptée de six est incluse dans Django à partir de la version 1.4.2. Vous pouvez l’importer depuis django.utils.six.

Voici les modifications les plus fréquemment nécessaires pour écrire du code compatible.

Gestion des chaînes

Les types basestring et unicode ont été supprimés en Python 3, et la signification de str a changé. Pour tester ces types, utilisez les idiomes suivants :

isinstance(myvalue, six.string_types)       # replacement for basestring
isinstance(myvalue, six.text_type)          # replacement for unicode
isinstance(myvalue, bytes)                  # replacement for str

Python ≥ 2.6 dispose de l’alias bytes pour str, ce qui fait que vous n’avez pas besoin de six.binary_type.

long

Le type long n’existe plus en Python 3. 1L est une erreur de syntaxe. Utilisez le test six.integer_types pour savoir si une valeur est un nombre entier ou un nombre entier long :

isinstance(myvalue, six.integer_types)      # replacement for (int, long)

xrange

Si vous utilisez xrange avec Python 2, importez six.moves.range et utilisez plutôt cette fonction. Il est aussi possible d’importer six.moves.xrange (qui est équivalent à six.moves.range), mais la technique précédente permet de simplement supprimer l’importation au moment de laisser tomber la prise en charge de Python 2.

Modules déplacés

Certains modules ont été renommés dans Python 3. Le module django.utils.six.moves (basé sur le module six.moves) constitue un emplacement d’importation compatible.

PY2

Si vous avez besoin de différencier le code entre Python 2 et Python 3, testez six.PY2:

if six.PY2:
    # compatibility code for Python 2

Il s’agit d’une solution de dernier recours lorsque six ne fournit pas de fonction correspondante.

La version adaptée de six dans Django

La version de six livrée avec Django (django.utils.six) contient quelques adaptations à usage purement interne.

Back to Top