Python 3.15 est sorti le 9 octobre. Sous Windows, un fichier ouvert sans encodage passe par défaut en UTF-8

Le vendredi 9 octobre, Python 3.15.0 est sorti. La page de la version compte 5 643 commits de 1 012 contributeurs. Huit jours plus tôt, le 1er octobre, Python 3.10 avait atteint sa fin de vie : 3.10.22 a été sa dernière version de sécurité, et la branche est désormais gelée.
Pour qui est encore sur 3.10, la question n'est donc plus de savoir s'il faut migrer. C'est où atterrir, et ce qui casse en chemin.
Beaucoup de la colle autour de mon travail est du Python : les scripts qui publient ces articles, la passe de fact-check où Jev juge mes brouillons, quelques tâches planifiées. Pour 3.15, j'ai donc lu le bas de la page What's New avant la visite des nouveautés : les sections Removed et Porting. C'est là que se cache le coût de la migration.
Le défaut qui a changé sous chaque script
La ligne la plus lourde de conséquences n'est pas dans la section Porting. Elle figure sous « Other language changes » : Python utilise désormais UTF-8 comme encodage par défaut, « indépendamment de l'environnement du système ». Un open() en mode texte sans argument encoding lit et écrit maintenant en UTF-8.
Sous Linux et macOS, ça ne change presque rien. La PEP 686, qui a pris la décision, le dit sans détour : « ce changement concerne surtout les utilisateurs de Windows ». La documentation Windows explique pourquoi. Windows « utilise toujours des encodages hérités pour l'encodage système (la page de code ANSI) », et jusqu'à 3.15, c'est ce que Python employait pour les fichiers texte quand aucun encodage n'était donné.
La PEP est tout aussi claire sur le risque : quand un programme dépend de l'encodage par défaut, le changement « peut provoquer une UnicodeError, du mojibake, voire une corruption silencieuse de données ».
Imagine un script sur un PC Windows dans une petite entreprise, qui lit un CSV écrit par un vieux logiciel dans la page de code du système, avec des noms accentués dedans. Sous 3.14, il le lit sans problème. Sous 3.15, le même open() lève une UnicodeDecodeError au premier « é », et tout fichier que Python réécrit est maintenant en UTF-8, que l'ancien outil risque de lire comme du charabia. Personne n'a touché au code. La PEP cite aussi les tubes de sous-processus en mode texte (text=True) parmi les cas à corriger.
Les correctifs sont tous documentés :
- Passe
encoding="utf-8"là où tu veux de l'UTF-8, etencoding="locale"(pris en charge depuis 3.10) là où tu veux vraiment l'encodage de la machine. Sous Windows, le codec « mbcs » te donne toujours la page de code ANSI. - Pour trouver chaque appel qui s'appuie sur le défaut, lance tes tests avec
-X warn_default_encodingou la variable d'environnementPYTHONWARNDEFAULTENCODING. Cet avertissement existe depuis 3.10, donc tu peux l'activer sur ta version actuelle, avant de migrer. - En échappatoire,
PYTHONUTF8=0ou-X utf8=0rétablit l'ancien comportement. Je le traiterais comme un ticket à ouvrir, pas comme un correctif.
Un format de date qui lève maintenant une erreur
Dans la section Removed : datetime.strptime() lève maintenant une ValueError quand le format contient %d sans année. C'était déprécié depuis 3.13.
La raison est un bug qui n'apparaît que les jours bissextiles. Sans année dans le format, strptime en met une par défaut, 1900, qui n'est pas bissextile. Analyse « 29/02 » avec "%d/%m" et ça échoue, alors que tous les autres jours de l'année passent. Le contournement documenté est d'inclure toujours une année, et si tes chaînes n'en ont pas, d'ajouter toi-même une année bissextile avant l'analyse.
Si tu extrais des dates comme « 14/07 » de documents, d'e-mails ou de noms de fichiers, cherche dans tes formats les %d sans %Y ni %y. Ce code n'échouait que les jours bissextiles. Sous 3.15, il échoue à chaque appel, ce qui est mieux, tant que tu le trouves avant la production. Cherche aussi %e : sans année, il émet maintenant un DeprecationWarning, avec une suppression prévue pour 3.17.
Le reste de la liste que je passerais au grep
Les sections Porting, Removed et les notes de modules sont longues, mais une poignée de lignes touchent du code d'application ordinaire :
sqlite3.connect()n'accepte plus que le chemin de la base en argument positionnel.sqlite3.connect("app.db", 10)ne fonctionne plus ; écristimeout=10.- Dans
argparse,add_argument('-f', '-foo')stocke maintenant la valeur sousfoo, pas sousf. Passe undestexplicite pour garder l'ancien nom. base64.urlsafe_b64decode()n'exige plus de bourrage (padding). Si ta validation comptait sur le rejet d'une entrée sans bourrage, passepadded=True.assertWarns()deunittestn'avale plus les avertissements qui ne correspondent pas. Une suite qui traite les avertissements comme des erreurs peut passer au rouge sans changement de code.importlib.metadata.metadata()lèveMetadataNotFoundquand le fichier de métadonnées d'une distribution est absent, là où il renvoyait un objet vide.
Puis les suppressions, qui mordent surtout par les vieilles dépendances plus que par ton propre code : les modules sre_compile, sre_constants et sre_parse, la méthode load_module() des chargeurs d'import (dépréciée depuis 3.4), CGIHTTPRequestHandler et l'option --cgi de http.server, la syntaxe à arguments nommés de NamedTuple. C'est ton fichier de verrouillage (lockfile) que je regarderais en premier.
Ce qui justifie le déplacement
Les imports paresseux sont la vedette, à juste titre. Écris lazy import json et le module se charge à la première utilisation du nom, pas au démarrage. La PEP 810 affirme que cela « peut réduire le temps de démarrage de 50 à 70 % en pratique » pour les outils en ligne de commande, et que des économies de mémoire de 30 à 40 pour cent « ont été observées sur des charges réelles ». Ce sont les chiffres de la PEP, pas les miens ; -X importtime montre où part ton propre temps de démarrage.
Trois réserves tirées de la spécification. lazy ne fonctionne qu'au niveau du module : dans une fonction, un corps de classe ou un bloc try/except, c'est une SyntaxError, donc le repli classique pour une dépendance optionnelle reste immédiat. Un import paresseux qui échoue échoue désormais à la première utilisation, peut-être au fond d'une requête, au lieu du lancement. Et les modules qui s'enregistrent à l'import, le motif du registre de plugins, ne s'enregistreront pas tant que rien ne les touche.
Deux gains plus discrets. Les binaires Windows 64 bits officiels utilisent maintenant l'interpréteur à appels terminaux : la page What's New rapporte une accélération de 15 à 20 pour cent sur la moyenne géométrique de pyperformance sous Windows x86-64 par rapport à l'interpréteur à switch-case, mesurée sur un AMD Ryzen 7 5800X, et des gains observés de 14 à 40 pour cent selon la charge. Aucun changement de code requis. Et Tachyon, le nouveau profileur par échantillonnage de profiling.sampling, s'attache à un processus en cours par son PID, avec un mode dump qui affiche la pile de chaque thread. Pour une tâche qui se bloque sur un serveur en pleine nuit, c'est la fonction que j'irais chercher en premier.
Le JIT est amélioré mais toujours expérimental : 7 à 8 pour cent sur la moyenne géométrique sous Linux x86-64, avec des benchmarks individuels allant d'environ 15 pour cent plus lents à plus de deux fois plus rapides. Pas une raison de migrer à lui seul.
La leçon de 3.14
Une ligne de la section gc mérite plus d'attention qu'elle n'en aura. Python 3.14.0 à 3.14.4 ont livré un nouveau ramasse-miettes incrémental. Après « un certain nombre de signalements de fortes pressions mémoire en environnement de production », il a été remplacé par le ramasse-miettes de 3.13, celui qu'utilisent 3.14.5 et les suivantes, ainsi que 3.15.
Cinq versions d'une branche stable ont tourné avec un ramasse-miettes qui a ensuite été retiré. Ce n'est pas une raison d'éviter 3.15. C'est une raison de la mettre sur un seul canari avec du vrai trafic et de surveiller la mémoire avant de la déployer partout.
Ce que je ferais ce mois-ci
Si quoi que ce soit tourne encore sur 3.10, migre-le maintenant, vers 3.14 ou 3.15. Il ne reçoit plus de correctifs de sécurité.
Avant de toucher à 3.15, lance la suite de tests sur ta version actuelle avec -X warn_default_encoding et corrige chaque avertissement avec un encodage explicite. Passe tes formats strptime au grep. Compare la liste de portage ci-dessus à ton code et à ton lockfile.
Ensuite un service, un canari, du vrai trafic, une semaine de courbes mémoire. La première version corrective, 3.15.1, est prévue le 1er décembre. Après cela, 3.15 reçoit des versions correctives pendant environ deux ans, puis des correctifs de sécurité en sources seules jusqu'en octobre 2031 environ : il n'y a donc aucune urgence à être le premier, seulement à quitter 3.10.
Le mot-clé dont tout le monde parlera est lazy. Le changement le plus susceptible de casser un script qui marche est celui que personne n'a tapé : un défaut.
Sources
- Python Software Foundation, "Python 3.15.0" (9 octobre 2026)
- Python documentation, "What's new in Python 3.15" (consulté le 19 octobre 2026)
- Python Enhancement Proposals, "PEP 686: Make UTF-8 mode default" (consulté le 19 octobre 2026)
- Python documentation, "Using Python on Windows" (consulté le 19 octobre 2026)
- Python documentation, "io: Core tools for working with streams" (consulté le 19 octobre 2026)
- Python documentation, "datetime: Basic date and time types" (consulté le 19 octobre 2026)
- Python Enhancement Proposals, "PEP 810: Explicit lazy imports" (consulté le 19 octobre 2026)
- Python Enhancement Proposals, "PEP 790: Python 3.15 Release Schedule" (consulté le 19 octobre 2026)
- Python Enhancement Proposals, "PEP 619: Python 3.10 Release Schedule" (consulté le 19 octobre 2026)
