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:
- Wrong assembly. A GRCh37 coordinate interpreted as GRCh38 lands on a different base.
- Wrong accession version.
NM_000546.5and.6can number differently. - Strand error. A minus-strand gene where someone forgot to reverse-complement — the ref base comes out as its complement.
- 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