Regex per il codice fiscale
Aggiornato il 30 luglio 2026 · 7 minuti di lettura
Nessuna espressione regolare può validare un codice fiscale. Una regex verifica la forma, cioè quali caratteri stanno in quali posizioni, mentre la validità dipende dal carattere di controllo, che è il risultato di una somma su tutti i primi quindici caratteri. È un calcolo, non uno schema. La regex corretta esiste ed è utile, ma è il primo di due passaggi, non l’unico.
La regex della forma
Questa è l’espressione che gira in produzione su questo sito: è copiata testualmente dal modulo che alimenta il decoder in home page, non riscritta per l’articolo.
/^[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$/Va applicata a una stringa già portata in maiuscolo e ripulita: raw.toUpperCase().replace(/[^A-Z0-9]/g, ''). I codici fiscali si scrivono spesso con spazi o trattini, e normalizzare prima evita di dover ammorbidire lo schema.
Segmento per segmento, contando le posizioni da uno:
| Pos. | Classe | Significato |
|---|---|---|
| 1-6 | [A-Z]{6} | consonanti di cognome e nome, tre e tre |
| 7-8 | [0-9LMNPQRSTUV]{2} | ultime due cifre dell’anno |
| 9 | [A-Z] | lettera del mese, dodici valori in uso |
| 10-11 | [0-9LMNPQRSTUV]{2} | giorno, con 40 sommati per le donne |
| 12 | [A-Z] | prima lettera del codice catastale |
| 13-15 | [0-9LMNPQRSTUV]{3} | le tre cifre del codice catastale |
| 16 | [A-Z] | carattere di controllo |
Il punto che distingue questa regex dalla maggior parte di quelle in circolazione è la classe [0-9LMNPQRSTUV] al posto di [0-9]. Le dieci lettere aggiuntive sono quelle che l’omocodia mette al posto delle cifre: 0 diventa L, 1 diventa M, 2 diventa N e così via fino a 9 che diventa V. La tabella completa delle sostituzioni
Variante con gruppi nominati
Se oltre a verificare la forma vuoi estrarre i segmenti, i gruppi nominati fanno le due cose in un colpo solo. Questa variante è anche leggermente più severa dell’altra: vincola la lettera del mese alle dodici effettivamente in uso.
const CF_SEGMENTI =
/^(?<cognome>[A-Z]{3})(?<nome>[A-Z]{3})(?<anno>[0-9LMNPQRSTUV]{2})(?<mese>[ABCDEHLMPRST])(?<giorno>[0-9LMNPQRSTUV]{2})(?<comune>[A-Z][0-9LMNPQRSTUV]{3})(?<controllo>[A-Z])$/;
const m = CF_SEGMENTI.exec('MLLSNT82P65Z404U');
console.log(m.groups);
// {
// cognome: 'MLL',
// nome: 'SNT',
// anno: '82',
// mese: 'P', // settembre
// giorno: '65', // 65 - 40 = 25, quindi femminile
// comune: 'Z404',
// controllo: 'U'
// }In Python la sintassi dei gruppi nominati è (?P<nome>…) anziché (?<nome>…):
import re
CF_SEGMENTI = re.compile(
r"^(?P<cognome>[A-Z]{3})(?P<nome>[A-Z]{3})"
r"(?P<anno>[0-9LMNPQRSTUV]{2})(?P<mese>[ABCDEHLMPRST])"
r"(?P<giorno>[0-9LMNPQRSTUV]{2})"
r"(?P<comune>[A-Z][0-9LMNPQRSTUV]{3})(?P<controllo>[A-Z])$"
)
m = CF_SEGMENTI.match("MLLSNT82P65Z404U")
print(m.groupdict())
# {'cognome': 'MLL', 'nome': 'SNT', 'anno': '82', 'mese': 'P',
# 'giorno': '65', 'comune': 'Z404', 'controllo': 'U'}Perché la forma non basta
Il modo più rapido per convincersene è guardare due codici che differiscono per un solo carattere:
const FORMA =
/^[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$/;
FORMA.test('RSSMRA85M01H501Q'); // true, ed è un codice valido
FORMA.test('RSSMRA85M01H501Z'); // true, ma NON è un codice valido
// Un solo carattere di differenza. Il sedicesimo è Z, ma il calcolo
// sui primi quindici caratteri restituisce Q. Nessuna espressione
// regolare può accorgersene: dovrebbe fare una somma.Non è un caso limite costruito ad arte: è la conseguenza normale di fermarsi al primo passaggio. Fra i servizi di decodifica del codice fiscale più diffusi ce ne sono che accettano RSSMRA85M01H501Z e ne restituiscono data e luogo di nascita senza alcun avviso, perché la forma è corretta e nessun controllo successivo viene fatto.
Oltre al carattere di controllo, ecco che cos’altro sfugge per costruzione a qualunque espressione regolare:
- Le date inesistenti. Il 31 febbraio si scrive B31 e passa qualsiasi schema.
- I giorni fuori intervallo. I valori ammessi sono 01-31 per gli uomini e 41-71 per le donne. Un 00, un 38 o un 99 hanno la forma giusta e nessun significato.
- Il codice catastale. Che Q999 non corrisponda ad alcun comune né ad alcuno Stato lo si scopre solo consultando una tabella.
- L’attribuzione effettiva. Un codice può essere formalmente e aritmeticamente perfetto senza essere mai stato assegnato a nessuno. Questo non lo stabilisce alcun algoritmo: solo l’Anagrafe tributaria.
Il secondo passaggio, quello che la regex non può fare, è una trentina di righe in tutto:
const DISPARI = {
'0': 1, '1': 0, '2': 5, '3': 7, '4': 9, '5': 13, '6': 15, '7': 17, '8': 19, '9': 21,
A: 1, B: 0, C: 5, D: 7, E: 9, F: 13, G: 15, H: 17, I: 19, J: 21, K: 2, L: 4, M: 18,
N: 20, O: 11, P: 3, Q: 6, R: 8, S: 12, T: 14, U: 16, V: 10, W: 22, X: 25, Y: 24, Z: 23,
};
const PARI = {
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9, K: 10, L: 11, M: 12,
N: 13, O: 14, P: 15, Q: 16, R: 17, S: 18, T: 19, U: 20, V: 21, W: 22, X: 23, Y: 24, Z: 25,
};
function checkChar(primi15) {
let somma = 0;
for (let i = 0; i < 15; i++) {
somma += i % 2 === 0 ? DISPARI[primi15[i]] : PARI[primi15[i]];
}
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[somma % 26];
}
/* La validazione completa e quindi in due tempi. */
function valido(cf) {
return FORMA.test(cf) && checkChar(cf.slice(0, 15)) === cf[15];
}
valido('RSSMRA85M01H501Q'); // true
valido('RSSMRA85M01H501Z'); // falseLe due tabelle non sono un dettaglio implementativo. Lo stesso carattere vale in modo diverso a seconda che si trovi in posizione dispari o pari, ed è precisamente questa asimmetria a rendere rilevabile lo scambio di due caratteri adiacenti: con una tabella sola la somma non cambierebbe e l’errore passerebbe sempre inosservato. Misurato su duecentomila casi generati, il carattere di controllo intercetta il 98,3% degli scambi fra caratteri adiacenti e il 98,4% degli errori di battitura su un singolo carattere. Il calcolo del carattere di controllo, passo per passo. La versione completa, con la decodifica di data, sesso e comune, è nel validatore in JavaScript e in quello in Python.
Gli errori più comuni
Sei classi di schemi difettosi che si incontrano di continuo, dal più dannoso al meno. Il primo è l’unico che rifiuta codici veri; gli altri accettano codici falsi.
1. Pretende cifre nelle posizioni numeriche
^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$È l’errore di gran lunga più diffuso, e l’unico che rifiuta codici realmente attribuiti. Ogni codice omocodico contiene lettere al posto di alcune cifre: RSSMRA85M01H5LMT viene respinto come “formato non valido” pur essendo perfettamente regolare.
2. Conta solo i caratteri
^[A-Za-z0-9]{16}$Accetta sedici zeri, sedici lettere A e qualunque altra stringa alfanumerica della lunghezza giusta. Non verifica nemmeno che i primi sei caratteri siano lettere.
3. Senza ancoraggio
[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]Priva di ^ e $, combacia con qualsiasi sottostringa: “XXRSSMRA85M01H501QSPAZZATURA” passa. In JavaScript succede ogni volta che si dimentica un’ancora; in Python succede anche usando re.search al posto di re.fullmatch.
4. Case-insensitive senza normalizzare
^[A-Z]{6}…[A-Z]$/iAccettare il minuscolo va benissimo, purché la stringa venga poi portata in maiuscolo. Se il codice prosegue con le tabelle del carattere di controllo, che sono indicizzate in maiuscolo, la ricerca fallisce e la somma diventa NaN, o peggio zero.
5. Non vincola la lettera del mese
…[A-Z][0-9LMNPQRSTUV]{2}…Le lettere valide sono dodici (A B C D E H L M P R S T), non ventisei. Con [A-Z] passa anche RSSMRA85W01H501U, dove la W non corrisponde ad alcun mese. Non è un difetto teorico: il carattere di controllo di quel codice è corretto, quindi nemmeno il checksum lo scarta e la stringa arriva intatta al resto del programma. La variante con gruppi nominati lo evita a costo zero.
6. Confonde i due formati
^\d{11}$ applicato a un CF di persona fisicaLe persone fisiche hanno un codice alfanumerico di sedici caratteri; enti e società ne hanno uno numerico di undici. Sono due cose diverse con due cifre di controllo diverse: un validatore che ne accetta uno solo rifiuta metà del mondo.
Trappole che dipendono dal linguaggio
Lo schema è lo stesso ovunque, il comportamento del motore no. Queste tre differenze in Python producono falsi positivi silenziosi, e le prime due si manifestano esattamente nello scenario più comune: leggere codici fiscali da un file.
import re
FORMA = re.compile(r"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z]"
r"[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$")
# 1. In Python "$" combacia anche PRIMA di un "\n" finale.
# Una riga letta da un file passa la validazione con il newline attaccato.
bool(FORMA.match("MRTMTT25D09F205Z\n")) # True <-- inatteso
# Rimedio: usare \Z, oppure re.fullmatch su un pattern senza ancore.
FORMA_Z = re.compile(r"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z]"
r"[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]\Z")
bool(FORMA_Z.match("MRTMTT25D09F205Z\n")) # False
# 2. In Python "\d" combacia con qualsiasi cifra Unicode, non solo con 0-9.
CON_D = re.compile(r"^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$")
bool(CON_D.match("RSSMRA٨٥M٠١H٥٠١Q")) # True <-- cifre arabo-indiane
# Rimedio: il flag re.ASCII, oppure la classe esplicita [0-9].
bool(re.compile(CON_D.pattern, re.ASCII).match(
"RSSMRA٨٥M٠١H٥٠١Q")) # False
# 3. Sotto re.IGNORECASE, [a-z] combacia anche con caratteri Unicode
# che non sono lettere latine: il segno kelvin U+212A, per esempio.
bool(re.match(r"^[a-z]$", "K", re.IGNORECASE)) # True <-- inatteso
bool(re.match(r"^[a-z]$", "K", re.IGNORECASE | re.ASCII)) # FalseIn JavaScript la trappola classica è un’altra, e produce falsi negativi:
// Il flag "g" rende .test() dipendente dallo stato: lastIndex avanza
// a ogni corrispondenza e viene azzerato solo quando fallisce.
const FORMA_G =
/^[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$/g;
FORMA_G.test('MRTMTT25D09F205Z'); // true
FORMA_G.test('MRTMTT25D09F205Z'); // false <-- stesso input, esito diverso
// Su un elenco di codici, questo scarta esattamente un codice valido su due.
// Rimedio: non usare mai il flag "g" con .test(), oppure azzerare lastIndex.In PHP, infine, la regex va racchiusa fra delimitatori e conviene aggiungere il modificatore D, che impedisce a $ di combaciare prima di un a capo finale, lo stesso problema visto in Python.
Il codice fiscale degli enti è un’altra cosa
Società, associazioni ed enti hanno un codice fiscale numerico di undici cifre, che per i soggetti titolari coincide con la partita IVA. La forma è banale (^\d{11}$), ma la cifra di controllo si calcola con un algoritmo mod 10 di tipo Luhn, che non ha nulla in comune con il carattere di controllo dei sedici caratteri.
Se il tuo campo deve accettare entrambi, servono due schemi e due funzioni di verifica distinte, scelte in base alla lunghezza dell’input normalizzato. Una regex unica che copra i due formati esiste, ma è solo un’alternanza dei due: non fa risparmiare nulla e nasconde il fatto che i controlli successivi sono diversi.
Domande frequenti
| Verificato su | Esito |
|---|---|
| MRTMTT25D09F205Z | forma e carattere di controllo corretti |
| FOXDRA26C24H872Y | forma e carattere di controllo corretti |
| MLLSNT82P65Z404U | forma e carattere di controllo corretti |
| RSSMRA85M01H5LMT | omocodico di secondo livello, accettato |
| RSSMRA85M01H501Z | forma corretta, carattere di controllo errato |
Ogni espressione e ogni frammento di codice di questa pagina è stato eseguito su questi casi prima della pubblicazione. Fonti e metodo