Le sujet du hash Python prête souvent à confusion parce qu’un même mot recouvre deux réalités: le hachage interne utilisé par dict et set, et les empreintes calculées avec hashlib pour vérifier un fichier, un message ou une donnée sensible. Je vais clarifier cette différence, montrer quels algorithmes choisir selon le cas, et détailler les usages où la sécurité impose de changer complètement d’approche. Le but est simple: vous éviter de choisir une fonction trop faible, trop lente ou tout simplement inadaptée.
Le bon choix dépend surtout de l’usage
-
hash()sert aux structures internes de Python, pas à fabriquer une empreinte persistante. -
hashlibproduit des digests reproductibles pour les fichiers, les API et les contrôles d’intégrité. -
sha256reste le choix le plus simple pour l’interopérabilité;blake2betblake2ssont très utiles quand je contrôle tout l’environnement. - Pour les mots de passe, j’utilise
pbkdf2_hmacouscryptavec un salt d’au moins 16 octets. - Pour comparer des empreintes,
hmac.compare_digest()est préférable à==.
Ce que fait réellement le hachage en Python
Je sépare toujours le sujet en deux. hash() renvoie un entier exploité par les structures de données de Python pour accélérer les recherches, alors que hashlib calcule un digest reproductible à partir d’octets. En pratique, hash() sert à l’implémentation interne de dict et set, tandis que hashlib.sha256() ou hashlib.blake2b() servent à comparer des empreintes, vérifier l’intégrité d’un fichier ou échanger une valeur stable entre deux systèmes.
Un point important me semble souvent mal compris: les chaînes et les bytes sont hachés avec une randomisation par exécution, ce qui limite certains abus sur les tables de hachage. Je n’utilise donc jamais hash() pour stocker une valeur sur disque ou l’échanger entre deux machines. Les nombres égaux partagent d’ailleurs le même hash, ce qui explique le comportement cohérent de 1 et 1.0 comme clés.
Si je pars d’un texte, je le convertis d’abord en octets avec UTF-8. Ensuite je choisis entre digest() pour un résultat binaire et hexdigest() pour une représentation lisible à afficher, journaliser ou conserver dans une base de données.
import hashlib
texte = "bonjour"
empreinte = hashlib.sha256(texte.encode("utf-8")).hexdigest()Cette distinction paraît basique, mais elle règle déjà la moitié des erreurs que je vois en revue de code. Une fois ce tri fait, le vrai choix devient celui de l’algorithme.
Choisir l’algorithme adapté à l’usage
Quand l’usage est clair, je compare les algorithmes sur trois critères: compatibilité, sécurité et contraintes d’exploitation. Pour un projet qui doit durer ou traverser plusieurs outils, je privilégie souvent un digest standard comme SHA-256; pour un système où je contrôle tout, BLAKE2 est très intéressant; et pour les mots de passe, je sors complètement de la logique du simple hash.
| Usage | API Python | Ce que j’en attends | À ne pas faire |
|---|---|---|---|
| Empreinte d’intégrité | hashlib.sha256() |
Large compatibilité, sortie de 32 octets, 64 caractères hexadécimaux | L’utiliser pour protéger un mot de passe |
| Cache, déduplication locale, empreinte interne |
hashlib.blake2b() ou hashlib.blake2s()
|
Digest configurable et API moderne dans la bibliothèque standard | Supposer que le même format conviendra à tous les systèmes tiers |
| Contexte où une famille SHA-3 est requise | hashlib.sha3_256() |
Une option standardisée et bien identifiée | Choisir SHA-3 juste parce qu’elle “sonne” plus moderne |
| Mot de passe |
hashlib.pbkdf2_hmac() ou hashlib.scrypt()
|
Un mécanisme lent, salé et paramétrable | Faire sha256(mot_de_passe)
|
| Compatibilité héritée |
hashlib.md5() ou hashlib.sha1()
|
Interopérabilité avec un système ancien ou un checksum non hostile | Les traiter comme des choix de sécurité |
J’ajoute une nuance utile en production: si je dépends d’un algorithme optionnel, je vérifie sa présence via hashlib.algorithms_available. Si je veux un code plus portable, je reste sur les constructeurs garantis. Et quand l’algorithme est connu à l’avance, j’utilise un constructeur nommé plutôt que hashlib.new(), parce que c’est plus clair et plus direct. Le bon outil est donc surtout celui qui colle au contexte d’échange, ce qui amène naturellement à la question des fichiers volumineux.

Hacher un fichier sans charger tout le contenu en mémoire
Dès qu’un fichier devient un peu gros, je passe en lecture par blocs. Charger l’ensemble en mémoire n’apporte rien ici et peut coûter cher pour un ISO, une archive ou un export de logs. La règle est simple: ouvrir le fichier en mode binaire, lire par morceaux, mettre à jour le digest à chaque bloc, puis produire le résultat final à la fin.
import hashlib
def empreinte_fichier(chemin):
h = hashlib.sha256()
with open(chemin, "rb") as f:
for bloc in iter(lambda: f.read(8192), b""):
h.update(bloc)
return h.hexdigest()
print(empreinte_fichier("archive.zip"))Le bloc de 8192 octets n’est pas une obligation absolue, mais c’est un bon point de départ simple et lisible. Sur une version récente de Python, hashlib.file_digest() va encore plus loin en encapsulant cette logique, ce que j’apprécie quand je veux du code concis et fiable.
import hashlib
with open("archive.zip", "rb") as f:
digest = hashlib.file_digest(f, "sha256").hexdigest()
print(digest)Je garde toutefois la boucle manuelle quand je dois cibler des environnements plus anciens ou quand je veux maîtriser très précisément le flux de lecture. Une fois ce mécanisme posé, la vraie rupture arrive avec les mots de passe, car là la vitesse n’est plus un avantage mais un risque.
Protéger un mot de passe demande plus qu’un simple digest
Je ne stocke jamais un mot de passe avec un hash rapide comme SHA-256, même si l’empreinte paraît propre et courte. Le problème n’est pas la collision, mais la rapidité: une fonction trop rapide facilite les attaques par essais massifs. Pour ce cas, je veux une fonction de dérivation de clé, avec un salt unique et un coût de calcul adapté au serveur.
Dans la pratique, je pars souvent sur hashlib.pbkdf2_hmac() si je veux quelque chose de simple, largement connu et facile à auditer. Si je veux une option mémoire-dure, hashlib.scrypt() est aussi une alternative sérieuse. Dans les deux cas, je génère un salt d’au moins 16 octets avec os.urandom(), je conserve le salt avec le hash, et je compare les résultats avec hmac.compare_digest() pour éviter un piège de temporisation. Quand j’ai besoin d’authentifier un message avec une clé secrète, je passe d’ailleurs à hmac plutôt qu’à un digest brut.
import os
import hashlib
import hmac
def hacher_mot_de_passe(mot_de_passe):
salt = os.urandom(16)
derive = hashlib.pbkdf2_hmac(
"sha256",
mot_de_passe.encode("utf-8"),
salt,
300_000,
)
return salt, derive
def verifier_mot_de_passe(mot_de_passe, salt, attendu):
test = hashlib.pbkdf2_hmac(
"sha256",
mot_de_passe.encode("utf-8"),
salt,
300_000,
)
return hmac.compare_digest(test, attendu)Le chiffre d’itérations n’est pas une formule magique: je le valide selon la charge réelle, le temps de réponse accepté et la machine qui exécute l’authentification. Ce qui compte, c’est le principe: ralentir volontairement l’attaquant sans dégrader inutilement l’expérience utilisateur. À partir de là, un dernier point mérite d’être clarifié: les objets Python eux-mêmes peuvent devenir hashables, mais seulement si on respecte leurs règles internes.
Rendre ses objets hashables sans casser les collections
Je rencontre souvent le même bug: une classe définit __eq__() pour comparer son contenu, puis l’équipe s’étonne qu’elle ne puisse plus servir de clé dans un dictionnaire. C’est normal. Un objet hashable doit garder une valeur de hachage stable pendant toute sa vie utile; si ses attributs changent, la table de hachage se désorganise. C’est pour cela que les listes et les dictionnaires ne sont pas hashables, alors que les tuples et les frozenset peuvent l’être si leurs éléments le sont aussi.
Quand je dois créer un type clé, je pars de champs immuables ou j’utilise un modèle figé comme une dataclass frozen. Si je définis manuellement __eq__(), je définis aussi __hash__() de façon cohérente, et je m’assure que les attributs pris en compte ne changent pas après insertion dans un set ou un dict.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: intSi je veux un exemple plus explicite, je peux aussi construire le hash à partir d’un identifiant réellement stable, comme un email normalisé ou un UUID. En revanche, je n’utilise jamais un attribut mutable pour cela, parce qu’un changement mineur suffit à rendre l’objet introuvable dans une collection. Et il reste un détail discret mais important: Python tronque le résultat d’un __hash__() personnalisé à la taille d’un mot machine, donc je n’essaie pas de réutiliser ce nombre comme identifiant persistant entre architectures.
Dans les projets sérieux, c’est ce niveau de discipline qui évite les incohérences difficiles à diagnostiquer. Avant de fermer le sujet, je garde quelques réflexes simples qui m’épargnent les erreurs les plus coûteuses.
Les réflexes que je garde avant de valider un hachage
-
hash()pour les structures internes de Python,hashlibpour les empreintes reproductibles. - Texte vers octets avant de hacher: j’utilise presque toujours UTF-8.
- SHA-256 ou BLAKE2 pour l’intégrité, PBKDF2 ou scrypt pour les mots de passe.
-
hmac.compare_digest()pour comparer des valeurs sensibles. - Objets immuables seulement si je veux qu’ils soient hashables.
-
hmacdès qu’une clé secrète doit authentifier un message, pas seulement le résumer.
Si je devais résumer ma pratique en une ligne, je dirais ceci: je choisis d’abord le bon usage, ensuite seulement la fonction. C’est ce tri qui rend le hachage utile en Python, au lieu d’en faire un faux sentiment de sécurité ou un détail d’implémentation mal maîtrisé.