Cette cheatsheet Python regex rassemble les 20 patterns que j'utilise vraiment en 2026, tout droit sortis du module re. À parser des logs à 2 h du matin quand quelque chose part en vrille, à valider des formulaires que personne n'apprécie tant qu'ils ne cassent pas. Chaque pattern arrive avec le pattern brut, ce qu'il fait réellement, un exemple que j'ai fait tourner, l'edge case qui va te mordre, et la librairie que je dégainerais quand le regex ne vaut plus le combat. Je les teste sur Python 3.12 et plus. Ils suivent des habitudes apprises à la dure : des raw strings à chaque fois, des anchors quand je valide, des non-capturing groups quand je ne fais que grouper.
The short answer
Une cheatsheet Python regex de 20 patterns re testés pour e-mail, IPv4 et IPv6,
URL, téléphone, ISO 8601, UUID, découpage CSV, lignes de log, slugs et plus. Chacun
arrive avec le pattern brut, un exemple qui matche, et l'edge case qui va te piéger.
Quand tu valides, ancre avec ^ et $ ; vire les anchors pour re.search ou
re.findall.
Comment lire les cartes
Chaque carte a la même forme. Le pattern re brut, ce à quoi je m'en sers, une entrée qui matche, l'edge case que j'ai laissé de côté exprès (et pourquoi il va te piéger), puis vers quoi je me replie une fois que le regex ne vaut plus le coup. Un truc à surveiller. Quand je valide, j'ancre tout avec ^ et $, comme ça la chaîne entière doit matcher d'un bout à l'autre. Tu veux extraire ces patterns d'un plus gros bloc de texte avec re.search ou re.findall ? Vire les anchors d'abord. Sinon tu vas rester planté là à te demander pourquoi rien ne matche, ce que, ouais, j'ai déjà fait.
Les 20 patterns
1. Validation d'e-mail
r"^[\w.+-]+@[\w-]+(\.[\w-]+)+$"
Ce qu'il fait : Attrape l'adresse de tous les jours. Lettres, chiffres, points de part et d'autre de l'arobase, le plus et le tiret autorisés aussi, et un domaine qui porte au moins un point. C'est celui que j'envoie vraiment en prod.
Exemple : alice.smith+work@example.co.uk matche.
Edge case : Il ne touchera pas aux quoted local parts du RFC 5321 ni aux domaines internationalisés les plus tordus. Et franchement ? C'est très bien comme ça. Tente de loger un parser RFC complet dans un regex et tu vas perdre, promis. Celui-ci couvre ce dont ton appli a besoin.
Alternative : email.utils.parseaddr si tu veux juste un parsing canonique. Le package PyPI email-validator quand tu as réellement besoin de la conformité RFC.
2. Adresse IPv4
r"^(?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d)$"
Ce qu'il fait : Vérifie que chaque octet tient vraiment dans 0-255, du coup les saletés genre 999.0.0.1 ou 1.2.3.4.5 se font rejeter. Cette plage d'octets est toute la raison pour laquelle ce truc a l'air aussi moche.
Exemple : 192.168.1.42 matche ; 256.0.0.1 non.
Edge case : Il avale joyeusement les zéros en tête genre 010.0.0.1. Un petit piège discret, celui-là. Certains parsers lisent 010 comme de l'octal et te voilà pointé vers un hôte que tu n'as jamais voulu toucher. Enlève les zéros d'abord si c'est dans ton périmètre.
Alternative : Pour de la pure validation, je filerais simplement la chaîne à ipaddress.IPv4Address(s) en attrapant la ValueError. Plus propre, et ça ne peut pas te mentir comme le regex en est capable.
3. Adresse IPv6 (simplifiée)
r"^(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}$"
Ce qu'il fait : Matche une adresse IPv6 écrite en entier. Les 8 groupes hex, des deux-points entre eux, rien de replié.
Exemple : 2001:0db8:85a3:0000:0000:8a2e:0370:7334 matche.
Edge case : Voilà le hic, et il est de taille. Dans la vraie vie, l'IPv6 ne s'écrit presque jamais au complet. La forme compressée :: lui passe sous le nez. Pareil pour l'IPv4-mapped ::ffff:1.2.3.4 et le suffixe de zone %eth0. Je ne sortirais celui-ci que quand je sais déjà que l'entrée est sous forme développée.
Alternative : ipaddress.IPv6Address(s) gère correctement chaque forme légale. Pour l'IPv6 je ne m'embête même plus avec du regex.
4. URL (http / https)
r"^https?://[\w.-]+(?:\.[a-zA-Z]{2,})+(?:[/?#][^\s]*)?$"
Ce qu'il fait : Attrape les URL http et https qui portent un vrai TLD, avec le path, la query et le fragment optionnels qui pendouillent au bout.
Exemple : https://example.com/path?id=42#section matche.
Edge case : Il ne connaît que http et https. Balance-lui du ftp, du mailto ou une data: URI et tu n'auras rien en retour. Il ne comprend pas non plus l'userinfo user:pass@ ni les domaines IDN. Parfait pour un link checker. Mauvais pour tout ce qui doit avaler des URL arbitraires.
Alternative : urllib.parse.urlparse(s) n'échoue jamais, il se contente de parser. Du coup vérifie le scheme et le netloc qu'il te renvoie et décide toi-même.
5. Numéro de téléphone au format E.164
r"^\+[1-9]\d{6,14}$"
Ce qu'il fait : Vérifie la forme E.164. Un signe plus, puis un indicatif pays qui ne peut pas commencer par zéro, puis 6 à 14 chiffres. C'est le format que tu veux voir posé dans une base de données.
Exemple : +33612345678 matche.
Edge case : Il vérifie la forme, pas la réalité. +19999999999 passe tout droit, et aucun humain ne pourrait jamais composer ça. Si "ça ressemble à un numéro de téléphone" te suffit, envoie. Mais ne va pas raconter à tes utilisateurs que c'est vérifié, parce que ça ne l'est pas.
Alternative : Quand j'ai vraiment besoin de faire confiance au numéro, je sors le package phonenumbers de Google. Il connaît les règles de longueur et de préfixe propres à chaque pays que le regex n'a jamais pu connaître.
6. Datetime ISO 8601
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$"
Ce qu'il fait : Matche le timestamp ISO 8601 canonique, fractions de seconde et décalage de fuseau tous les deux optionnels. Le format que les API te servent à longueur de journée.
Exemple : 2026-05-27T14:30:00.123+02:00 matche.
Edge case : Le regex compte des chiffres, pas des calendriers. Du coup 2026-02-30 se faufile tranquillement. Février a son avis là-dessus. Le pattern ne le partage pas. Si une date bidon peut t'arriver, valide après le match, pas à la place.
Alternative : Sur 3.11+ j'appelle juste datetime.fromisoformat(s). Il bouffe chaque variante d'ISO 8601 et rejette les dates qui ne peuvent pas exister. C'est ce que je ferais.
7. Date JJ/MM/AAAA (européenne)
r"^(0[1-9]|[12]\d|3[01])/(0[1-9]|1[0-2])/\d{4}$"
Ce qu'il fait : Valide le format européen JJ/MM/AAAA. Maintient le jour et le mois dans des plages sensées au lieu de laisser passer n'importe quels deux chiffres au hasard.
Exemple : 27/05/2026 matche ; 32/05/2026 non.
Edge case : Même piège que celui d'ISO. 31/02/2026 matche, parce que le pattern n'a aucune idée que février s'arrête à 28 ou 29. Si le calendrier compte pour toi, finis le boulot avec datetime.strptime(s, "%d/%m/%Y").
Alternative : datetime.strptime lève une ValueError sur une date qui ne peut pas exister. Ce qui est exactement ce que tu veux pour une validation stricte.
8. Heure HH:MM:SS (24 heures)
r"^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$"
Ce qu'il fait : Valide une horloge 24 heures. Heures 00-23, minutes et secondes plafonnées à 00-59. Aucune absurdité genre 25:00 ne passe.
Exemple : 14:30:00 matche ; 25:00:00 non.
Edge case : Il rejette les secondes intercalaires (23:59:60), que quelques standards de timestamp autorisent réellement. Tu n'en croiseras probablement jamais. Mais si tu parses des données issues d'un système qui en émet, tu vas silencieusement jeter des lignes valides sans jamais savoir pourquoi.
Alternative : datetime.time.fromisoformat quand tu veux un parsing plus strict qu'un regex qui se contente de compter des chiffres.
9. UUID v4 (aléatoire)
r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
Ce qu'il fait : Vérifie spécifiquement la disposition v4. Le nibble de version doit être 4, et le nibble de variant doit tomber sur 8, 9, a ou b. Du coup il ne laissera pas passer un UUID v1 random déguisé pour faire illusion.
Exemple : 550e8400-e29b-41d4-a716-446655440000 matche.
Edge case : En minuscules uniquement. File-lui un UUID en majuscules et il hausse simplement les épaules. Celui-là me mord plus souvent que je ne voudrais l'admettre. Ajoute re.IGNORECASE ou passe à [0-9a-fA-F] et continue ta journée.
Alternative : uuid.UUID(s) se fiche de la version comme de la casse. Il te dit juste si c'est un UUID, point final.
10. Code couleur hexadécimal
r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
Ce qu'il fait : Matche les couleurs hex CSS dans les longueurs que tu colles vraiment. La forme courte à 3 chiffres (#f00), celle à 6 chiffres (#ff0000), plus le RGBA à 8 chiffres (#ff0000ff).
Exemple : #1e293b matche.
Edge case : Il saute la forme courte RGBA à 4 chiffres (#f00f), dont, honnêtement, j'oublie l'existence à peu près une fois sur deux. Si tes design tokens s'appuient dessus, glisse un {4} dans l'alternation et te voilà couvert.
Alternative : Le CSS moderne te laisse aussi écrire des couleurs nommées et la syntaxe en fonction genre rgb() et oklch(). Le regex ne suivra pas tout ça. S'il te faut tout couvrir, parse le CSS pour de vrai.
11. Découpage de ligne CSV avec champs entre guillemets
r'(?:^|,)("(?:[^"]|"")*"|[^,]*)'
Ce qu'il fait : Parcourt les champs d'une ligne séparée par des virgules et gère la partie pénible. Les champs entre guillemets qui cachent des virgules à l'intérieur, et l'échappement par "" doublé par-dessus.
Exemple : alice,"smith, jr.",42 donne trois captures.
Edge case : Deux trous, et tu vas les sentir vite. Il ne sait pas gérer un retour à la ligne à l'intérieur d'un champ entre guillemets, le genre où un enregistrement CSV déborde sur plusieurs lignes. Il laisse aussi les guillemets autour collés sur les captures, à toi de les retirer. Le vrai CSV fait ces deux trucs en permanence.
Alternative : Écoute, utilise juste csv.reader. C'est dans la stdlib, ça gère chacun de ces edge cases, et je ne touche au regex que quand importer csv donnerait l'impression d'un overkill ridicule.
12. Entrée de log Apache ou nginx au format common
r'^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) (\S+)" (\d+) (\S+)'
Ce qu'il fait : Extrait les sept champs d'une ligne au format common log : IP du client, timestamp, méthode, path, protocole, statut, octets envoyés. C'est celui que j'ai collé dans plus de scripts jetables que n'importe quoi d'autre sur cette liste.
Exemple : 127.0.0.1 - - [27/May/2026:14:30:00 +0200] "GET /index.html HTTP/1.1" 200 1234 matche.
Edge case : Il s'arrête au format common. Du coup les extras du format combined, le referer et le user-agent, tombent simplement au bout. La plupart des serveurs que je touche loggent en combined de toute façon, alors je rajoute "([^"]*)" "([^"]*)" et je récupère ces deux-là aussi.
Alternative : Parfait pour un grep ponctuel. Mais dès que tu parses des logs à un vrai volume, envoie-les vers un truc comme Loki avec un vrai pipeline de parsing. Un regex bricolé par serveur ne passe pas à l'échelle, et tu vas en vouloir à sa maintenance d'ici un mois.
13. Réduction des espaces
re.sub(r"\s+", " ", text).strip()
Ce qu'il fait : Écrase chaque suite d'espaces, tabulations et retours à la ligne compris, en un seul espace, puis rogne les extrémités. Mon one-liner de prédilection pour nettoyer la sortie d'OCR ou du HTML scrapé. Et aussi tout ce qu'un utilisateur vient de coller dans un champ.
Exemple : " Hello\t\n world " devient "Hello world".
Edge case : Lance-le sur du HTML brut et il va joyeusement aplatir aussi les espaces à l'intérieur de tes blocs <pre>, ce qui massacre tes échantillons de code. Pointe-le uniquement vers le contenu textuel. Jamais vers le markup.
Alternative : " ".join(text.split()) fait exactement la même chose sans aucun regex, et c'est un poil plus rapide sur les chaînes courtes. Une fois sur deux c'est ça que je dégaine en réalité.
14. Extraction de lien Markdown
r"\[([^\]]+)\]\(([^)]+)\)"
Ce qu'il fait : Découpe un lien Markdown en ses deux moitiés utiles. Le label visible dans le group 1, l'URL dans le group 2, à partir d'un truc genre [label](https://example.com).
Exemple : See [the docs](https://docs.example.com) donne le label "the docs" et l'URL "https://docs.example.com".
Edge case : Il casse à la seconde où un crochet apparaît dans le label ou une parenthèse dans l'URL. Et les URL Wikipedia sont absolument truffées de parenthèses, du coup ça échoue bien plus souvent que tu ne le devinerais. Il ne sait pas compter les crochets imbriqués. Aucun regex ne le peut vraiment, ce n'est pas un défaut que tu peux corriger.
Alternative : Pour du Markdown que tu n'as pas écrit toi-même, file le boulot à un vrai parser comme mistune ou markdown-it-py et arrête de deviner.
15. Nombre JSON
r"-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?"
Ce qu'il fait : Matche un nombre tel que la spec JSON le définit réellement. Signe optionnel, pas de zéro en tête en douce, puis une décimale et un exposant optionnels. Il colle à la grammaire, ce qui est tout l'intérêt.
Exemple : -1.23e-4 matche ; 007 non.
Edge case : Il ne matche pas Infinity, ni NaN, ni un littéral hex. C'est voulu, parce qu'aucun de ces trucs n'est du JSON légal au départ. Si ton "JSON" se balade avec ça, quelque chose en amont te ment déjà.
Alternative : Quand c'est un document entier au lieu d'un token isolé, laisse tomber le regex et laisse json.loads(s) faire le parsing.
16. Identifiant Python (ASCII uniquement)
r"^[A-Za-z_][A-Za-z0-9_]*$"
Ce qu'il fait : Vérifie le nom de variable classique en ASCII uniquement. Commence par une lettre ou un underscore, puis lettres, chiffres ou underscores tout le long.
Exemple : my_var2 matche.
Edge case : Le Python moderne est parfaitement content avec des noms Unicode comme élève ou π, et ce pattern claque la porte à chacun d'entre eux. Du coup si tu acceptes l'Unicode, ce regex va rejeter du code qui tourne absolument bien. Agaçant.
Alternative : Laisse tomber le regex carrément et appelle "name".isidentifier(). C'est le contrôle officiel, et il connaît déjà les règles Unicode sur le bout des doigts.
17. Chemin de fichier Unix
r"^/(?:[^/\x00]+/)*[^/\x00]*$"
Ce qu'il fait : Matche un chemin Unix absolu. Des slashs avant, et pas d'octets nuls planqués dans les segments.
Exemple : /home/user/file.txt matche.
Edge case : Lis celui-ci deux fois si la sécurité t'importe. Il accepte joyeusement les segments .. et ., ce qui est exactement la façon dont une attaque par path-traversal s'évade du répertoire dans lequel tu voulais la confiner. "Ça a matché mon regex de chemin" n'est pas un contrôle de sécurité, point final. Passe la chose dans pathlib.Path.resolve() et confirme où elle atterrit vraiment avant d'en faire confiance à un seul octet.
Alternative : pathlib.PurePosixPath(s) parse la chaîne sans jamais toucher au système de fichiers.
18. Chemin de fichier Windows
r'^[A-Za-z]:\\(?:[^\\/:*?"<>|]+\\)*[^\\/:*?"<>|]*$'
Ce qu'il fait : Matche un chemin Windows absolu. Lettre de lecteur, séparateurs en backslash, et il rejette les caractères que Windows interdit dans un nom de fichier.
Exemple : C:\Users\admin\file.txt matche.
Edge case : Les chemins Windows sont un marécage. Ce pattern rate les partages UNC (\\server\share) et le préfixe de chemin long (\\?\). Il ignore aussi le fait que Windows accepte discrètement les slashs avant. Je me suis fait avoir par les trois, séparément, des mauvais jours. Ne couvre que les cas que tu es sûr de croiser vraiment.
Alternative : pathlib.PureWindowsPath(s) parse sans approcher le disque, et il connaît les bizarreries que ce regex ignore.
19. Mot de passe robuste
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$"
Ce qu'il fait : Impose les règles de composition habituelles avec quatre lookaheads. Au moins 12 caractères, et un de chaque : minuscule, majuscule, chiffre, plus un symbole.
Exemple : P@ssw0rd-StrongEnough matche.
Edge case : Voilà la vérité qui dérange, et je suis peut-être minoritaire sur le volume sonore à y mettre. Cocher ces cases n'a presque rien à voir avec le fait qu'un mot de passe soit réellement robuste. P@ssw0rd1234 passe chacune des règles ici et traîne dans pratiquement tous les dumps de fuite d'internet. Les règles de composition mesurent à quel point un mot de passe est obéissant. L'entropie, c'est une autre question. Couple toujours ça avec une vérification contre une liste connue de mots de passe ayant fuité.
Alternative : zxcvbn-python estime réellement à quel point un mot de passe est devinable, et il repère les mots du dictionnaire et les keyboard walks qu'un regex ne verra jamais.
20. Slug compatible URL
r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
Ce qu'il fait : Valide un slug propre en minuscules. Des groupes alphanumériques reliés par des traits d'union simples, sans trait d'union en tête ni en fin, et aucun doublé au milieu.
Exemple : my-awesome-post-2026 matche ; --bad-- non.
Edge case : Il valide un slug. Il n'en fabrique pas. File-lui "Café Brûlé" et il dit simplement non, parce que les accents le bloquent net. Tu dois normaliser avec unicodedata.normalize d'abord pour retirer les diacritiques, puis valider le résultat.
Alternative : Pour générer les slugs pour de vrai je ne réinvente rien de tout ça. python-slugify gère la normalisation, la translittération et le plafonnement de longueur en un seul appel.
Les patterns à éviter en 2026
Quelques regex "classiques" continuent de se faire copier-coller depuis des réponses StackOverflow vieilles de dix ans, et chacun m'a brûlé au moins une fois. Prends le regex "e-mail parfait", ce monstre de 1 000 caractères qui essaie d'être le RFC 5322 sur une seule ligne. Il rejette de vraies adresses qui délivrent du courrier très bien. Utilise un pattern court et pragmatique, puis envoie un e-mail de confirmation et basta. La boîte de réception est le seul validateur qui compte vraiment. Ensuite il y a l'"URL qui matche tout", avec chaque scheme, l'userinfo et l'IDN boulonnés dessus. C'est fragile, et urllib.parse.urlparse le bat de toute façon sur la vitesse comme sur la correction. Et le "validateur de carte bancaire" qui flaire les marques de carte par les préfixes de leur numéro ? Celui-là est pire qu'inutile. Il te file un petit sentiment de sécurité chaud que tu n'as pas mérité. Le contrôle de Luhn te dit qu'un numéro est bien formé. Seule la banque te dit qu'il est réel, et un regex tout nu n'arrive même pas à gérer la première partie.
Notes de performance pour le regex à fort volume
Compile une fois, réutilise pour toujours. C'est l'unique habitude qui paie à chaque fois. Mets re.compile(pattern) tout en haut au niveau du module et appelle PATTERN.match(s) dans ta boucle chaude. Oui, re met déjà en cache les patterns compilés en interne. Mais s'appuyer sur ce cache dans une boucle serrée perd quand même de façon mesurable face à une bonne vieille constante au niveau du module, et l'intention se lit plus clairement de toute façon. Une fois passé le cap de millions de matchs par seconde, sors le package PyPI regex. Ses possessive quantifiers et ses atomic groups coupent le catastrophic backtracking avant qu'il ne puisse figer tout le process. Et si tu as un ensemble fixe de patterns et que tu n'as pas besoin de named captures, les bindings hyperscan peuvent te donner à peu près 100x. Celui-là je le garde pour quand le profiler pointe réellement dessus. Pas une seconde avant, parce que c'est une vraie dépendance à porter.
Sources et lectures complémentaires
Questions fréquentes
Pourquoi utiliser des raw strings (r"...") pour chaque pattern ?
Parce que les backslashs veulent dire quelque chose à la fois pour Python et pour le moteur regex, et c'est exactement dans ce double sens que se cachent les bugs. Sans le préfixe r, une séquence comme \n se transforme en un vrai retour à la ligne avant même que le regex y jette un œil. Le préfixe raw fait que ton pattern se lit exactement comme la doc d'où tu l'as tiré, du coup je mets r sur chaque pattern, sans exception.
Quand le regex est-il le mauvais outil ?
Dès que le truc s'imbrique. HTML, JSON, tout ce qui a une grammaire récursive est littéralement impossible à parser correctement avec du regex, et c'est les vraies maths, pas moi qui fais le puriste. Les dates, les chemins et les UUID ont déjà une librairie chacun (datetime, pathlib, uuid) qui est plus solide et se lit mieux. Là où le regex justifie son existence, c'est sur le truc plat et bien défini.
Comment éviter le catastrophic backtracking ?
Commence par des non-capturing groups (?:...) quand tu n'as pas besoin de la capture. Les possessive quantifiers du module regex tuent le backtracking sur les portions greedy qui ont tendance à exploser. Puis j'attaque mon propre pattern exprès : file-lui un truc pathologique, genre la lettre a répétée une trentaine de fois, et regarde. S'il fige sur cette entrée jouet, il figera sur une requête hostile en prod.
Python regex supporte-t-il le look-behind ?
Ouais. Le look-behind à largeur fixe (?<=...) existe depuis toujours, et la version à largeur variable a débarqué dès Python 3.7. Les look-aheads (?=...) ont toujours marché aussi. Un conseil quand même : vas-y mollo avec. Empiler trois look-arounds dans un seul pattern est en général le jour où j'aurais dû le scinder en deux passes simples.
Et les classes Unicode comme \p{L} ?
La re de la bibliothèque standard ne fait pas du tout les classes de propriété \p{...}, du coup essayer en lève juste une erreur. Le module regex sur PyPI le fait, et c'est un remplaçant drop-in. Si je valide seulement de l'ASCII, la re toute simple suffit ; à la seconde où du vrai Unicode est sur la table, j'installe regex.