Reference material and a strict reference implementation for ISO 17442, the international standard for the Legal Entity Identifier (LEI), together with a published corpus of really-published non-conforming values taken from named public registers.
This is not another LEI validator. Validating an LEI is commodity code and several libraries already do it. What is provided here is a conformance model that distinguishes four outcomes instead of two, a test corpus whose every defect is a value some register actually published, and the measured finding that the widely used commodity libraries are built for a different job and will under-report defects if you point them at a register.
ISO 17442 is published by ISO/TC 68 (Financial services) in three parts. The single-part editions are withdrawn, and citations to them are stale.
| Part | Title | Status |
|---|---|---|
| ISO 17442-1:2020 | Financial services. Legal entity identifier (LEI). Part 1: Assignment | Current. Reviewed and confirmed 2026 |
| ISO 17442-2:2020 | Part 2: Application in digital certificates | Current |
| ISO 17442-3:2024 | Part 3: Verifiable LEIs (vLEIs) | Current. Published October 2024 |
| ISO 17442:2019 | Financial services. Legal entity identifier (LEI) | Withdrawn. Replaced by Parts 1 and 2 |
| ISO 17442:2012 | Financial services. Legal Entity Identifier (LEI) | Withdrawn. Revised as the 2019 edition |
The structure of the identifier has not changed across these editions. An LEI is
20 characters: 18 drawn from upper case A-Z and 0-9, followed by 2 decimal
check digits computed under ISO 7064 MOD 97-10.
Expand every character to its base-36 value as decimal digits (A becomes 10,
Z becomes 35), read the result as one large integer, and take it modulo 97.
For a conforming LEI the remainder is 1. Because MOD 97-10 can never yield
them, the check digit pair is never 00, 01 or 99, so those three values are
impossible rather than merely wrong.
Two consequences follow, and both matter more than they look. An LEI is self-validating: any holder can reject an impossible value with no lookup and no network call. And a truncated LEI is not a slightly damaged LEI, it is an unverifiable string, because truncation discards the very digits that make the identifier self-validating.
A boolean collapses distinct governance problems that need different responses.
| State | Meaning | What it usually indicates |
|---|---|---|
valid |
Well formed and the MOD 97-10 remainder is 1 | Conforming |
fails-check-digits |
Well formed, arithmetic rejects it | A recording error such as a transposition, or a letter O typed for a zero |
impossible-check-digits |
Well formed but ends 00, 01 or 99 |
A placeholder or filler value, never a damaged real LEI |
not-checkable |
Wrong length, or characters outside the repertoire | Usually truncation by a republisher. The defect belongs to the publisher, not to the Global LEI System |
from iso17442 import classify, Conformance classify("506700GE1G29325QX363") # Conformance.VALID classify("5493O00MN7XN3BBKCE67") # FAILS_CHECK_DIGITS (letter O for zero) classify("00000000000000000000") # IMPOSSIBLE_CHECK_DIGITS classify("7H6GLXDRUGQFU57R") # NOT_CHECKABLE (truncated to 16 characters)
python-stdnum provides stdnum.lei, and it is correct for the job it was
designed for. Its documented convention, shared across the whole library, is to
compact() the input first, stripping separators and surrounding whitespace and
upper-casing, and then validate. validate() returns the canonical form. That is
right for input validation, where a human may type an identifier with dashes
and you want to accept it.
It is wrong for publication auditing, where the deviation is the finding. Measured behaviour:
stdnum.lei.is_valid('5493000mn7xn3bbkce67') -> True
stdnum.lei.is_valid(' 506700GE1G29325QX363') -> True
ISO 17442 admits only upper case A-Z and 0-9, so neither value is a
conforming LEI. Audit a register with a normalising validator and every
lower-cased or space-padded value it publishes is silently counted as
conforming. This is not hypothetical: lower-case LEI values occur in published
US bank register data.
The rule this library follows is therefore: never normalise before assessing. Canonicalise afterwards if you need to, but the assessment must see exactly what was published.
Input validation and publication auditing pull in opposite directions. Almost every available implementation is built for the first. If you are measuring what a register published, check which one you are holding.
iso17442/vectors.py carries the test vectors, each with its expected state and
its provenance. Every defective value was published by a named register, not
invented, because a suite built only from synthetic defects tends to miss the
shapes registers actually emit. Truncation is the clearest example: it looks
nothing like a typo.
shapes/ carries the same rules for RDF data: SHACL Core shapes for the lexical
form, and the full ISO 7064 MOD 97-10 arithmetic as a SHACL-SPARQL constraint,
which is the only form that catches a transposition.
fibo/ carries a draft OWL datatype restriction for FIBO's
fibo-be-le-lei:LegalEntityIdentifier, offered in response to
edmcouncil/fibo#2238. FIBO
asserts ISO 17442 conformance in its definition but carries no syntactic
constraint, so a FIBO graph cannot currently distinguish a valid LEI from a
truncated one. Note the limitation: an xsd:pattern facet catches length,
character set and impossible check digits, but no regular expression can express
MOD 97-10 arithmetic, so a transposition still passes.
A US ISIN is the country code, then a 9 character national number, then a check digit, and for US securities that national number is the CUSIP. GLEIF publishes an ISIN to LEI mapping under CC0. Slicing characters 3 to 11 of the US rows yields a CUSIP to LEI crosswalk from public-domain inputs alone.
Measured on the GLEIF file of 8 August 2026: 9,119,948 rows, of which 2,281,318 are US, every one with a CUSIP-shaped national number, giving 2,281,318 distinct CUSIP-9 values across 29,651 CUSIP-6 issuer prefixes.
Null result: zero of those 2,281,318 CUSIP-9 values map to more than one issuer LEI. The instrument-level crosswalk is internally consistent.
Caution on the issuer level. 331 CUSIP-6 prefixes (1.12%) carry more than one LEI, and it is tempting to read that as register disagreement. It is not. Every case examined is a fund family: CUSIP-6 identifies the trust, while the LEI identifies the individual fund series, so the one-to-many relationship is correct. The two identifiers sit at different levels of the hierarchy. Anyone migrating from a CUSIP-6 entity key to an LEI should expect this and should not treat it as a data quality defect.
pip install -e .
python -m pytest tests/ -qCode and shapes are MIT. The conformance states, the corpus provenance and the
measurements above may be cited via CITATION.cff.