pythonforbio.
[WASM idle]
HGVS Nomenclature10/11

hgvs Validator

Starting sandbox…
Beginnerlesson

hgvs Validator

In one line: checks that a syntactically valid variant is also real — coordinates in range, reference bases matching the actual sequence.

Parsing tells you a string is well-formed. Validation tells you it describes something that exists.

The two levels

import hgvs.validator

hv = hgvs.validator.Validator(hdp)
hv.validate(var)                      # both levels; raises on failure
hv.validate(var, strict=False)        # returns False instead of raising
Validator Needs data? Checks
IntrinsicValidator no internal consistency: start ≤ end, ref present where required, sensible edit for the type
ExtrinsicValidator yes the accession exists, positions are within the sequence, ref bases match the actual sequence
Validator yes both
from hgvs.validator import IntrinsicValidator, ExtrinsicValidator

iv = IntrinsicValidator()             # fast, offline
ev = ExtrinsicValidator(hdp)          # needs UTA + SeqRepo

Running IntrinsicValidator first as a cheap filter is worthwhile on large batches — it catches malformed variants without a database round-trip.

What extrinsic validation catches

hv.validate(hp.parse_hgvs_variant("NM_000546.6:c.215A>G"))
# HGVSInvalidVariantError: Variant reference (A) does not agree with
# reference sequence (C)

That single check catches the four most common and most dangerous data errors:

  1. Wrong assembly. A GRCh37 coordinate interpreted as GRCh38 lands on a different base.
  2. Wrong accession version. NM_000546.5 and .6 can number differently.
  3. Strand error. A minus-strand gene where someone forgot to reverse-complement — the ref base comes out as its complement.
  4. A genuinely corrupt record. Transcription errors, Excel damage, copy-paste across rows.

All four are silent without validation, and all four produce results that look completely normal downstream.

The exception types

from hgvs.exceptions import (
    HGVSInvalidVariantError,     # ref mismatch, or intrinsically invalid
    HGVSInvalidIntervalError,    # position outside the sequence
    HGVSDataNotAvailableError,   # accession not in UTA/SeqRepo
    HGVSUnsupportedOperationError,
    HGVSError,                    # the base class
)

def validate_one(hv, var):
    try:
        hv.validate(var)
        return "valid", None
    except HGVSInvalidVariantError as e:
        return "ref_mismatch", str(e)
    except HGVSInvalidIntervalError as e:
        return "out_of_range", str(e)
    except HGVSDataNotAvailableError as e:
        return "no_data", str(e)
    except HGVSError as e:
        return "other", str(e)

Classifying by exception type is what turns "1,200 variants failed" into "1,150 are on transcripts absent from our UTA snapshot, 40 have ref mismatches, 10 are out of range" — three different problems with three different fixes.

Where validation belongs

var = hp.parse_hgvs_variant(s)        # 1. syntax
hv.validate(var)                       # 2. reality  ← HERE
var = hn.normalize(var)                # 3. canonical form
var_g = am.c_to_g(var)                 # 4. projection

Validate before normalizing and projecting. Both of those depend on the reference sequence, so if the ref base is wrong you get a confidently-computed wrong answer rather than an error.

Note that Normalizer(validate=True) and AssemblyMapper(prevalidation_level=...) do some of this for you, but an explicit validation step gives you a clean place to catch and classify failures.

Batch validation

def validate_batch(df, hp, hv, col="hgvs"):
    rows = []
    for s in df[col]:
        try:
            var = hp.parse_hgvs_variant(s)
        except HGVSParseError as e:
            rows.append({"status": "parse_error", "detail": str(e)})
            continue
        status, detail = validate_one(hv, var)
        rows.append({"status": status, "detail": detail})

    out = df.join(pd.DataFrame(rows, index=df.index))
    print(out["status"].value_counts())
    return out

checked = validate_batch(variants, hp, hv)
valid = checked.query("status == 'valid'")
print(f"{len(valid):,} of {len(checked):,} usable ({len(valid)/len(checked):.1%})")

Report the counts, and look at the failure patterns. Failures almost never scatter randomly — they cluster around one systematic cause, and the pattern tells you what it is:

  • Nearly all ref_mismatch → wrong assembly, or wrong accession versions throughout
  • Nearly all no_data → your UTA snapshot predates the transcript set the file uses
  • A few out_of_range → likely genuine data errors in those rows

Validation is not interpretation

The Validator answers: does this variant exist as described?

It does not answer:

  • Is this variant pathogenic? (→ ACMG criteria, ClinVar, InterVar)
  • Is it real, or a sequencing artefact? (→ variant caller quality metrics, IGV)
  • Is it the best transcript to report on? (→ MANE Select, clinical convention)
  • Does the described protein consequence occur? (→ functional data)

A variant can be perfectly valid HGVS, perfectly matched to the reference, and complete nonsense biologically. Validation is a data-integrity check, not a biological one.

Custom checks worth adding

Beyond what the library does:

def extra_checks(var, am, hdp):
    warnings = []

    # is the accession the MANE Select transcript for its gene?
    if var.type == "c" and var.ac not in MANE_TRANSCRIPTS:
        warnings.append(f"{var.ac} is not MANE Select")

    # is the variant deep intronic? (likely low interpretive value)
    off = getattr(var.posedit.pos.start, "offset", 0)
    if abs(off) > 20:
        warnings.append(f"deep intronic ({off:+d} from the splice site)")

    # very large indel — check it is intended
    edit = var.posedit.edit
    if edit.type in ("del", "delins", "dup"):
        span = var.posedit.pos.end.base - var.posedit.pos.start.base + 1
        if span > 100:
            warnings.append(f"large event ({span} bp)")

    return warnings

The MANE check is the most valuable in practice — a variant reported on a non-canonical transcript will not match the literature or ClinVar, even though it is perfectly valid.

Common mistakes

  • Skipping validation and computing on bad data.
  • Catching bare Exception, losing the classification.
  • Validating after normalizing.
  • Treating "valid" as "clinically meaningful".
  • Not reporting failure counts.
  • Not looking at failure patterns.
  • Assuming validation catches assembly mixups. It catches the ones where the ref base happens to differ — roughly 3 in 4. The rest pass silently. Track your assembly explicitly; do not rely on validation to catch it.

That last point deserves emphasis. If a GRCh37 coordinate is misread as GRCh38, the base at the wrong position matches the claimed ref about 25% of the time by chance. Validation will catch 75% of such errors — good, but not a substitute for knowing which assembly your data is in.

See also

hgvs Normalizer · hgvs Parser · SeqRepo · AssemblyMapper · hgvs Pitfalls · Applied - Variant Normalization Pipeline

lesson example

No output yet — run the code to populate this drawer.