Free API key, instantly: 1,000 calls a day. Get a free key, or use it with no key at 100 a day.
HLA-Verify

Guides › APIs and bioinformatics tools

How to validate HLA allele names in Python

Updated 2026-10-30 · allele examples checked against IPD-IMGT/HLA 3.66.0 on 2026-10-30

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:

InputResult
A*02:01:01:01assigned
HLA-A*02:01valid_prefix A*02:01
A*02:01:14Qrenamed to A*02:01:14
C*04:09Nrenamed to C*04:09L
A*9201renamed to A*02:101:01
A*2401deleted: "Sequence shown to contain errors (March 1995)"
DQB1*05:03:26:99unknown
A*02:01:01Gnot_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

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.