Guides › APIs and bioinformatics tools
How to validate HLA allele names in Python
To validate HLA allele names in Python, load Allelelist.txt, Allelelist_history.txt and Deleted_alleles.txt from one IPD-IMGT/HLA release, then classify each name: assigned in that release, a valid lower-resolution prefix, renamed, deleted, or unknown. A regular expression can reject malformed strings, but only the release files can tell you whether a well-formed name exists.
The code below uses only the standard library. It was run against the release 3.66.0 files and the outputs shown are what it printed.
Get the files
The files are in the ANHIG/IMGTHLA repository, one tag per release. Download them from the tag you want to pin, for example v3.66.0-alpha, rather than from the moving Latest branch, and keep them together in one folder. The file guide describes each file's columns.
Every file starts with # header lines; the # version: line names the release. Skip those lines before parsing the CSV.
Load the release
import csv
import re
NAME = re.compile(r"^(?:HLA-)?([A-Z][A-Z0-9]*)\*(\d+(?::\d+){0,3})([NLSCAQ]?)$")
def read_rows(path):
with open(path, encoding="utf-8") as fh:
yield from csv.DictReader(line for line in fh if not line.startswith("#"))
def load_release(folder):
current = {row["Allele"] for row in read_rows(f"{folder}/Allelelist.txt")}
deleted = {row["Allele"]: row["Description"]
for row in read_rows(f"{folder}/Deleted_alleles.txt")}
ids_for = {} # any name ever used -> set of HLA IDs
latest = {} # HLA ID -> name in this release, or None
for row in read_rows(f"{folder}/Allelelist_history.txt"):
hla_id = row.pop("HLA_ID")
names = list(row.values()) # newest release first
latest[hla_id] = None if names[0] == "NA" else names[0]
for name in names:
if name != "NA":
ids_for.setdefault(name, set()).add(hla_id)
return current, deleted, ids_for, latest
ids_for maps a name to a set of IDs, not one ID, on purpose. Names are not unique across the whole history: in the 3.66.0 history file A*30:100 belonged to one allele in releases 3.26.0 to 3.31.0 (that allele is now A*30:02:12) and to a different allele from 3.32.0 onwards. If you know the release a name was written in, read that release's column instead of the union.
Classify a name
def classify(name, current, deleted, ids_for, latest):
name = name.strip()
if not NAME.match(name):
return "not_an_allele_name", None # group codes, MACs, XX, typos
name = name.removeprefix("HLA-")
if name in current:
return "assigned", name
if any(c.startswith(name + ":") for c in current):
return "valid_prefix", name
if name in ids_for:
now = sorted(filter(None, (latest[i] for i in ids_for[name])))
return ("renamed", now) if now else ("deleted", deleted.get(name))
if name in deleted:
return "deleted", deleted[name]
return "unknown", None
current, deleted, ids_for, latest = load_release(".")
for n in ["A*02:01:01:01", "HLA-A*02:01", "A*02:01:14Q", "C*04:09N",
"A*9201", "A*2401", "DQB1*05:03:26:99", "A*02:01:01G"]:
print(n, classify(n, current, deleted, ids_for, latest))
Output with the 3.66.0 files:
| Input | Result |
|---|---|
A*02:01:01:01 | assigned |
HLA-A*02:01 | valid_prefix A*02:01 |
A*02:01:14Q | renamed to A*02:01:14 |
C*04:09N | renamed to C*04:09L |
A*9201 | renamed to A*02:101:01 |
A*2401 | deleted: "Sequence shown to contain errors (March 1995)" |
DQB1*05:03:26:99 | unknown |
A*02:01:01G | not_an_allele_name |
What this code deliberately does not do
It does not expand reporting codes. A*02:01:01G fails the name pattern because it is a G group, a set defined in wmda/hla_nom_g.txt, not an allele. Handle G and P groups, XX codes and multiple allele codes in their own branch; the G and P group files guide has a parser.
It does not convert pre-2010 spellings mechanically. Colon-less names are looked up in the history as written. That handles rollover names such as A*9201, which became A*02:101 and then A*02:101:01, but it also means a colon-less spelling is matched to the specific allele that carried that exact name in an early release. If your data uses colon-less names as two-field typings, convert them first by inserting colons, as described in the conversion guide, and then classify the result.
It does not drop suffixes. A*24:09N is assigned. In this code, A*24:09 without the N is not a prefix of any current name and comes back unknown, rather than being quietly matched to the null allele. The pattern keeps the suffix as part of the name.
It is not fast for huge batches. The prefix test scans the allele list. For millions of names, build a set of every prefix of every current name once.
It does not know which release you meant. Pin the folder to a tag and record that release with every result.
Doing the same through an API
If you would rather not maintain the files, one HTTP call can classify a whole batch. This client uses only the standard library. It was tested against a local instance of the HLA-Verify service pinned to release 3.66.0; HLAVERIFY_BASE lets you point it at one:
import json
import os
import urllib.request
API = os.environ.get("HLAVERIFY_BASE", "https://api.hlaverify.com")
def normalize(typings, api_key=None):
"""One POST /v1/normalize call for a whole batch of reported names."""
headers = {"content-type": "application/json",
"User-Agent": "my-lab-pipeline/1.0"}
if api_key:
headers["X-API-Key"] = api_key
body = json.dumps({"typings": typings}).encode()
req = urllib.request.Request(f"{API}/v1/normalize", data=body, headers=headers)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.load(resp)
result = normalize(["A*0101", "Cw*07:02", "A*24:09N", "A*02:AB"])
print(result["release"])
for row in result["rows"]:
print(row["reported"], "->", row["current_name"], row["flags"])
It printed release 3.66.0, then A*0101 to A*01:01 with deprecated_name, Cw*07:02 to C*07:02 with deprecated_name, A*24:09N unchanged with null_allele, and A*02:AB as UNRESOLVABLE with mac_code.
Anonymous use allows 100 calls a day per IP address, so send one batch per call rather than one name per call; a batch can hold up to 250 names without a key. A rate-limited call returns HTTP 429 with a Retry-After header. Send allele names only, never patient identifiers. The API guide covers the other endpoints.
Frequently asked questions
Can I validate HLA allele names with a regular expression?
Only their shape. A pattern rejects malformed strings, but names such as DQB1*05:03:26:99 are well formed and do not exist, so the check has to use a release's allele list.
Which IPD-IMGT/HLA files do I need to validate names in Python?
Allelelist.txt for current names, Allelelist_history.txt for renamed names and Deleted_alleles.txt for deletion reasons, all from the same release.
Is an HLA allele name unique across releases?
Almost always, but not always. In the 3.66.0 history file, A*30:100 and C*08:276 were each used for two different alleles at different times, so record the release a name was written in.
How do I validate thousands of HLA names quickly?
Load the release files once and test names against an in-memory set, or send them in batches to an API that accepts a list per call.
Sources
- https://github.com/ANHIG/IMGTHLA/blob/Latest/README.md
- https://github.com/ANHIG/IMGTHLA/blob/Latest/Allelelist.txt
- https://hla.alleles.org/pages/nomenclature/naming_alleles/
- https://hlaverify.com/api
HLA-Verify is a research-and-evaluation tool for nomenclature and reference-release validation. It is not a medical device and this guide is not clinical advice. Send allele names, never patient identifiers.