Ce jour-là, je fais un git pull sur la branche de développement et je récupère vingt-trois commits, environ 1 400 lignes modifiées, dont je n’ai pas écrit une seule ligne.
Le projet est un logiciel de gestion de licences écrit en Django, que j’ai développé il y a quelques mois, seul et à la main, sans IA, du premier fichier au dernier.
Il était globalement terminé côté serveur et il attendait que mon client dégage du temps pour attaquer l’interface, qu’il développe lui-même en React à 100 %. Cette phase a démarré, et depuis, mon client code avec sa propre session Claude et pousse le résultat sur la branche que je récupère.
Les messages de commit annoncent des corrections sur les données incomplètes renvoyées par Cleverbridge et Nalpeiron, un KeyError sur les champs de contact et une recherche d’abonnement lancée sans identifiant. Django se lance sans erreur, et pour l’instant je n’en sais pas plus.
Puis j’ai lu le diff en entier, et comme il y a de quoi écrire plusieurs articles, je vais y aller épisode par épisode. Celui-ci ne parle que d’une seule fonction, de trois lignes, et de tout ce qu’elle a entraîné derrière elle.
Je donne au passage les prompts que j’ai utilisés, tels que je les ai tapés.
Pourquoi cette fonction existe
Il faut d’abord que j’explique à quoi elle sert, sinon la suite n’a aucun intérêt.
Le logiciel journalise énormément de choses, et notamment le contenu des échanges avec Cleverbridge et Nalpeiron, qui est enregistré en JSON, donc en texte, dans une colonne de la base.
Or le JSON ne connaît que cinq sortes de valeurs (des chaînes, des nombres, des booléens, des listes et des dictionnaires) et il n’a pas de type date, alors que les données à journaliser sortent des modèles Django et des validated_data de Django REST Framework, qui en contiennent forcément.
Donc json.dumps() refuse de les écrire :
>>> json.dumps({'created_at': datetime.now()})
TypeError: Object of type datetime is not JSON serializable
Vous vouliez tracer un échange avec l’API et vous récupérez une erreur qui parle du système de log, ce qui vous fait perdre l’information au moment précis où vous en aviez besoin.
Python fournit la parade avec le paramètre default, qui attend une fonction que json.dumps() n’appelle que pour les objets qu’il ne sait pas écrire lui-même, et qui transforme ici la date en texte à la norme ISO 8601 :
>>> json.dumps({'created_at': datetime.now()}, default=datetime_converter)
'{"created_at": "2026-08-24T15:27:49.812345"}'
Seulement il faut y penser à chaque appel, et il suffit d’un oubli dans un fichier pour que la ligne de log tombe en erreur ce jour-là, sur ce cas précis, en production.
D’où la fonction créée par la session de mon client, qui applique le convertisseur toute seule pour que plus personne n’ait à y penser :
def json_dump(value, **kwargs):
kwargs.setdefault('default', datetime_converter)
return json.dumps(value, **kwargs)
Vingt-quatre appels le précisent quand même
body = json_dump(response.data, default=datetime_converter)
**kwargs récupère les arguments nommés reçus par la fonction, et setdefault pose la valeur par défaut uniquement quand la clé est absente. Ici l’appelant écrit default=... lui-même, donc la clé est toujours fournie et setdefault n’a jamais l’occasion d’agir.
La fonction sérialise correctement, mais elle se comporte exactement comme le json.dumps() qu’elle remplace, donc l’oubli qu’elle devait rendre impossible reste possible partout, et il ne reste qu’une indirection en plus et rien de gagné.
C’est ce que j’ai vu en lisant le diff, avec ce premier prompt :
Le dernier git pull a récupéré du code modifié par claude avec des erreurs de conception, analyse le dernier diff et dis moi les erreurs et modifications inutiles
Je précise « erreurs de conception » et « modifications inutiles » parce que sans ces deux termes je n’obtiens rien, et des bugs il n’y en avait presque pas à trouver.
Puis :
explique moi la correction à faire avec json_dump et où est-elle appelée
La correction consiste à retirer le paramètre sur les appels à json_dump(), en faisant attention à ne pas le retirer des quatre json.dumps() restants où il reste indispensable :
# avant body = json_dump(response.data, default=datetime_converter) # après body = json_dump(response.data)
Un seul endroit décide alors comment les dates deviennent du texte, et le jour où un autre type pose le même problème, un Decimal ou un UUID, la correction se fait dans ces trois lignes et les vingt-quatre appels en profitent, alors qu’avec le paramètre recopié partout il faut rouvrir vingt-quatre fois le même sujet dans six fichiers.
Trois de ces vingt-quatre lignes étaient de moi
Avant de laisser corriger quoi que ce soit, j’ai voulu savoir d’où venaient ces appels :
c'est moi qu iavait ajouté default=datetime_converter) au départ ?
Il y en avait trois sur vingt-quatre, datés du 17 avril 2025, du 1er juillet 2025 et du 24 juillet 2025, et à l’époque ils étaient obligatoires puisque ces trois lignes appelaient json.dumps() directement et qu’elles seraient tombées en erreur sans le paramètre.
Les vingt et une autres viennent du commit du 14 août 2026, celui-là même qui crée la fonction, si bien que le commit a écrit le raccourci et recopié à la main, partout, l’argument que ce raccourci devait supprimer, en l’ajoutant au passage sur mes trois lignes qu’il convertissait.
C’est là que j’ai lancé la correction :
Ok, donc corrige les endroits où il est précisé en double
Et immédiatement après, parce que je ne valide pas un travail que je n’ai pas vu :
donne moi avant le diff de ce qui a été fait et je veux savoir systématiquement ce que tu changes
J’ai eu droit à un résumé en liste, qui n’est pas un diff :
Non ce n'est pas un diff, donne moi un diff
Puis à un fichier .patch écrit sur le disque, que je n’avais pas demandé :
Non n'écris pas de fichier patch, donne moi le diff ici
Trois allers-retours pour obtenir un git diff affiché à l’écran, et ça vaut le coup d’insister parce que c’est la seule façon de garder la main sur ce qui entre dans le dépôt.
La même fonction, écrite deux fois
Le convertisseur de dates existait à deux endroits, alors j’ai demandé depuis quand :
pourquoi la fonction datetime_converter s'est retrouvé en double, à quel moment ça s'est produit
La réponse ne m’a pas fait plaisir, parce que le doublon vient de moi.
Le 17 avril 2025, j’écris la fonction dans lib/errors.py, puis le 1er juillet j’en ai besoin ailleurs et j’en fais une copie au lieu de l’importer, et le 3 juillet je corrige en supprimant la copie et en plaçant la fonction dans lib/utils.py, au bon endroit.
Sauf que ce commit-là touchait quinze fichiers, donc l’originale d’avril est restée dans lib/errors.py et je ne l’ai jamais vue.
Elle y est restée treize mois, et le refactor du 14 août 2026 n’a rien créé de nouveau mais il a figé la situation, puisque le fichier importait désormais la nouvelle fonction tout en continuant d’appeler l’ancienne copie locale, les deux travaillant côte à côte dans le même fichier.
ok, nettoie ce code mort
Le ménage autour
En retirant l’argument partout, des imports sont devenus inutiles, puis des import json entiers dans des fichiers où plus rien n’utilisait json :
nettoyer aussi les import json inutiles
Dix-sept fichiers, dont un où le commit de mon client avait ajouté l’import sans jamais s’en servir, et comme cinq autres fichiers portaient le même import mort depuis plus longtemps, tant qu’à faire :
Corriger aussi les autres fichiers avant le git pull
La question qui a tout changé
Le nettoyage était fini, et j’ai quand même posé celle-là :
Le problème concernant le json dump est-il totalement réglé ?
La réponse est non.
Dans serializers/product.py, une ligne de log passe l’instance du modèle Django à la fonction, au lieu du JSON renvoyé par Nalpeiron :
log("License Details for '" + license_code + "'", level="debug", request=request,
verbose=json_dump(nalpeironlicensecode))
Le convertisseur ne sait convertir que des dates, donc sur une instance de modèle il lève une TypeError, et comme aucun try n’entoure la ligne, le serveur renvoie une erreur 500 provoquée par une ligne de log de niveau debug, alors que trois cents lignes plus bas, dans la même classe, le même besoin est traité correctement avec _j_licensecode.
Ok, corrige.
Mon métier au quotidien, c’est l’infogérance de serveurs Linux, et une erreur 500 provoquée par une ligne de log est exactement le type d’incident qui remonte côté serveur alors que la cause est dans le code applicatif : on cherche la charge, la base de données, le reverse proxy, et le coupable est dans un log(level="debug").
D’où venait cette ligne
Je voulais comprendre, parce que ce n’était pas du code de mon client :
Pourquoi on ne peut pas vraiment sérialiser le model, je le faisais avant pourtant
La première réponse m’a expliqué que je venais de PHP, où json_encode() accepte n’importe quel objet, et que j’avais dû porter mes habitudes, sauf que je ne fais pas de PHP :
Non, je ne fais pas de PHP mais je trouve curieux avoir mis ça au départ.
La correction est arrivée tout de suite, et cette fois avec l’historique : la ligne date du 27 janvier 2025, un jour où j’écris cinq cents lignes d’un coup et une série de logs bâtis sur le même moule, dont tous passent des dictionnaires sauf deux qui passent un objet, écrites à cent soixante lignes d’intervalle dans la même passe.
Elles ont ensuite voyagé pendant dix-huit mois, déplacées telles quelles par un gros refactor, jusqu’à ce qu’un commit du 4 août 2026 corrige la seconde sans voir la première.
Et si celle-ci ne s’est jamais déclenchée, c’est qu’elle se trouve dans une branche du code qui ne s’exécute que sur une licence réelle avec version, mais le jour où ce chemin passe, c’est une 500 en production à cause d’un log de débogage.
Retenez ce prompt, c’est celui qui rapporte le plus : demandez si le problème est totalement réglé, une fois que tout a l’air terminé.
Les commentaires et le commit
La fonction de trois lignes était accompagnée de huit lignes de docstring :
Ok, j'ai compris. Maintenant corrige les commentaires liés à ce besoin pour qu'ils soient court ou inexistant, je ne veux pas lire de roman dans le code
Il en reste une ligne.
Fais un git commit
Le message de commit contenait un titre, un paragraphe d’explication et quatre puces détaillant chaque point, ce qui est encore un roman :
Fais des commit plus court
Au final, 25 fichiers touchés, 26 lignes ajoutées, 65 supprimées, un seul commit, et trente-trois minutes entre le premier prompt de correction et la fin.
Expert Python et Django, en renfort
Je vends de l’infogérance de serveurs Linux, pas du développement, mais c’est pourtant du Python et du Django que je fais sur ce projet et je peux le faire ailleurs.
Si vous cherchez un développeur Python freelance pour tenir une application Django déjà en production, je peux intervenir en renfort, que ce soit pour relire le code que vous n’avez pas le temps de relire, corriger, préparer la mise en production ou tenir le serveur qui l’héberge, en complément de l’infogérance ou séparément.
La prochaine fois
Il reste huit autres trouvailles dans ce diff, entre une correction prévue pour un seul point d’entrée mais appliquée à toute l’API, une remontée de la pile Python à chaque ligne de log, une migration de base de données pour du confort de lecture et neuf fois la même ligne recopiée.
Ce sera l’épisode 2.
Sur ce projet, j’interviens sur du code écrit par quelqu’un d’autre, mais je peux aussi prendre une application Django de bout en bout, du premier fichier jusqu’au serveur Linux qui la fait tourner. Écrivez-moi.