Parsing guide

This page covers what parse returns and how to control it: the candidate types it ranks, how to restrict results when the input is untrusted, how to tell an explicit species from an inferred one, and how species= and default_species= differ. Read it before running mhcgnomes over data you did not curate yourself.

Parsing strategy

It is a fool's errand to curate all possible MHC allele names since that list grows daily as the MHC loci of more people (and non-human animals) are sequenced. Instead, MHCgnomes contains an ontology of curated species and genes and then attempts to parse any given string into multiple candidates of the following types:

The set of candidate interpretations for each string are then ranked according to heuristic rules. For example, a string will be preferentially interpreted as an Allele rather than a Serotype or Haplotype.

Which result type means what

Several result types are legitimate MHC designations without being molecules, which is why HLA-DR15 and BoLA-DR parse cleanly but yield no allele:

Type Means Example
Allele A specific allele HLA-A*02:01
Gene A locus, no allele given Patr-AL
Pair Class II alpha/beta pairing HLA-DPA1*01:03/DPB1*06:01
AlleleWithoutGene Allele name whose gene is unknown BoLA-D18.4
Class2Locus A class II locus family BoLA-DR
Serotype Serological group, may cover many alleles HLA-DR15
Haplotype Named haplotype H2-b
Supertype Functional grouping of alleles HLA-A02
MhcClass Just a class, with or without species HLA class I
Species Only a species was named HLA

One allele or an allele group?

An Allele can also be an allele group. In IMGT/HLA and IPD-MHC names the first field is the group and the second names a protein, so HLA-A*02 is a group and HLA-A*02:01 is one allele. is_allele_group classifies the written name; it does not validate a molecule. False does not establish registry membership, a complete sequence, normal expression, or suitability for a binding predictor. Field counts alone go wrong both ways: HLA-MICA*001 is one complete allele, and SLA-1*15XX is a group.

>>> from mhcgnomes import parse
>>> [parse(name).is_allele_group for name in ["HLA-A*02", "HLA-A*02:01", "H2-Kb"]]
[True, False, False]
>>> [parse(name).is_allele_group for name in ["HLA-MICA*001", "SLA-1*15XX"]]
[False, True]

Old names are recognized only after alias resolution. NetMHC's Mamu-A01 is an allele in the Mamu-A1*001:01 lineage once use_allele_aliases=True resolves it. Without alias resolution it stays the one-field Mamu-A*01 and reads as a group. Unknown names can still parse: validate the sequence and any downstream requirements separately. G/P groups also require separate handling.

>>> parse("Mamu-A01", use_allele_aliases=True).is_allele_group
False
>>> parse("Mamu-A01").is_allele_group
True

Parsing untrusted input

parse accepts anything that looks like MHC nomenclature, which is the right default for a known-good allele list and the wrong one for free text. Scanning curator notes or a spreadsheet column, "it parsed" is not the same as "this is an MHC molecule": a stray HLA class I is a valid MhcClass, and a bare d is a valid mouse Haplotype.

Say what you will accept, and let anything else come back as None:

from mhcgnomes import parse, Allele, Gene, Pair

result = parse(token, required_result_types=(Allele, Gene, Pair), raise_on_error=False)
if result is None:
    continue  # not an MHC molecule
HLA-A*02:01                -> Allele        n/a                 -> None
H2-K*b                     -> Allele        -                   -> None
Patr-AL                    -> Gene          HLA class I         -> None
HLA-DPB1*06:01/DPA1*01:03  -> Pair          d                   -> None

This is more robust than filtering on the returned type yourself, since it also prevents a lower-ranked interpretation from being chosen in the first place.

Was the species explicit, or guessed?

Species inference is right most of the time and load-bearing when it is wrong. A bare gene symbol can carry a species you never supplied, and the default species means a deliberately generic string still comes back with one:

>>> import mhcgnomes
>>> from mhcgnomes import parse
>>> parse("Gaga-BLB2*02").species_source
'explicit'
>>> parse("BLB2*02").species_source     # inferred from the gene name
'inferred'
>>> parse("MHC class II").species_source  # fell back to default_species
'default'

result.species_from_input is the boolean form, true only for explicit — it answers the question a caller actually has: did I supply this species, or did the parser? To reject anything you did not name outright — the right rule when validating curated data — ask for it at parse time:

>>> parse("BLB2*02", require_explicit_species=True, raise_on_error=False) is None
True

Provenance is not part of a result's identity: two alleles that differ only in how their species was determined still compare equal and hash the same. It is also computed only when asked, so it costs nothing if you never look.

Printing only the prefixes a source attests

HLA-A*02:01 and CyanCaer-DAB1 look alike, but only the first is published nomenclature — the second was minted here so the blue tit would have a unique prefix. prefix_provenance tells them apart, and an opt-in display policy acts on it:

>>> from mhcgnomes.prefix_display_policy import prefix_display_policy, ATTESTED
>>> parse("CyanCaer-DAB1").to_string()
'CyanCaer-DAB1'
>>> with prefix_display_policy(ATTESTED):
...     parse("CyanCaer-DAB1").to_string()
'CyanistesCaeruleus-DAB1'
>>> with prefix_display_policy(ATTESTED):
...     parse("HLA-A*02:01").to_string()   # designated, so untouched
'HLA-A*02:01'

The default is unchanged, both spellings parse under either policy, and group labels such as SLA and BoLA keep their labels. set_prefix_display_policy is the process-wide form; the policy is thread-local.

Gene symbols shared by several species

A gene symbol only carries a species when it belongs to one lineage. DRB1 is declared by 45 species across humans, cattle, dogs and horses, so on its own it names none of them:

>>> parse("DRB1*01:01", default_species=None, raise_on_error=False) is None
True
>>> parse("DRB1*01:01").to_string()  # with the default species, as before
'HLA-DRB1*01:01'
>>> parse("BLB2*02").to_string()  # chicken and the Galliformes node: one lineage
'Gaga-BLB2*02'

A species that uses a shared symbol but does not own its bare form says so in the ontology with context only, which is why BF2*02:01 still resolves to the chicken although the guineafowl also has a BF2.

Gene lookup is case- and separator-normalizing, so some symbols differ only in spelling. Where they do, your spelling decides:

>>> parse("Ia1").species.name   # Paralichthys olivaceus spells it this way
'Paralichthys olivaceus'
>>> parse("IA1").species.name   # Chrysolophus pictus spells it this way
'Chrysolophus pictus'
>>> parse("ia1", raise_on_error=False) is None  # neither, so neither is named
True

A species you name outright still wins over a spelling that points elsewhere — OS=Mus musculus GN=Mr1 is a mouse gene even though Rattus sp. is the entry that spells MR1 as Mr1.

Species-directed parsing

species= constrains parsing to a single species. The final parsed object must match that species exactly, or parsing fails. This is useful when you know the organism and want to reject cross-species mismatches:

>>> mhcgnomes.parse("BoLA-DRB3*01:01", species="Bos taurus").to_string()
'Bota-DRB3*01:01'
>>> mhcgnomes.parse("HLA-A*02:01", species="Bos taurus", raise_on_error=False) is None
True
>>> mhcgnomes.parse("A*02:01", species="Homo sapiens").species.name
'Homo sapiens'

When the input uses an ancestor prefix (like BoLA for genus-level Bos sp.), species= rewrites the result to the requested descendant species if valid.

default_species= is a less strict alternative — it provides a fallback species hint for inputs that don't contain a species prefix, but does not reject inputs that resolve to a different species:

>>> mhcgnomes.parse("A*02:01", default_species="Homo sapiens").species.name
'Homo sapiens'
>>> mhcgnomes.parse("DMA", default_species="Chelonia mydas").species.name
'Chelonia mydas'

Resolving legacy allele names

Alias resolution is opt-in: parse(name, use_allele_aliases=True). It follows curated renames to their terminal target. Lookup tries the supplied gene first, then its canonical name and sorted alternate names, so the answer does not depend on Python's hash seed.

Colons are meaningful in alias keys. A legacy digit string is looked up before it is split into modern fields:

>>> parse("HLA-A*02172", use_allele_aliases=True).to_string()
'HLA-A*02:17:02:01'
>>> parse("HLA-A*02:172", use_allele_aliases=True).to_string()
'HLA-A*02:172'

A modern two-field name is not expanded merely because its colon-free spelling is a legacy key. For example, HLA-B*08:01 retains two fields, while the old HLA-B*0801 resolves to HLA-B*08:01:01:01. Explicit modern-name renames still apply. Alias tables are curated historical data, not a live registry validator; see #238 for the remaining current-name conflicts found in the registry audit.

Missing inputs and DataFrames

parse, Parser.parse, and parse_gene_class treat None and scalar missing values such as float NaN, pandas.NA, and NaT consistently: they raise ParseError by default or return None with raise_on_error=False. infer_mhc_class returns None for these values. Other non-string inputs, including numbers, bytes, booleans, lists and arrays, always raise TypeError. Strings are not coerced; a literal "nan" remains text for the parser.

dataframe_from_string_list forwards parser options and preserves one row per input in tolerant mode:

>>> from mhcgnomes import dataframe_from_string_list
>>> frame = dataframe_from_string_list(
...     ["HLA-A*02:01", None, "zzz-not-mhc", "H2-b"], raise_on_error=False)
>>> frame["type"].tolist()
['Allele', 'ParseError', 'ParseError', 'Haplotype']
>>> frame["error"].iloc[1]
'Missing MHC name'

Pass species, use_allele_aliases, require_explicit_species, required_result_types, or other parse options as keywords. The default raise_on_error=True stops at the first parse failure. Tolerant mode catches only ParseError; invalid types and invalid options still raise.

Common columns describe the input, result type, normalized/compact names, species, short gene_name, MHC class, properties and error. Available legacy to_record() fields follow, including full gene and allele identifiers. Input values and order are preserved; a pandas Series' index is not reused. Empty input produces zero rows with the common outcome columns. Haplotype results have those common columns even without a record schema.

dataframe_from_parsed_objects accepts result objects and, with raise_on_error=False, None/ParseError outcomes. Its input column uses raw_string, or None when the original text is unavailable. Both DataFrames and CLI rendering share the public outcome-row API.