HGVS Nomenclature08/11
Starting sandbox…
Beginner·4 graded tests

HGVS Parser

hgvs Parser

Core Principle: HGVSParser uses a formal Parsing Expression Grammar (PEG) to parse HGVS strings into typed SequenceVariant object trees without requiring external database connections.

Basic use

[1]python
out1ms
'NM_000546.6:c.215C>G'

Construct the parser once and reuse it. Parser() compiles a PEG grammar, which takes on the order of a second. Doing it inside a loop over 10,000 variants is a self-inflicted 3-hour job.

Why not regex

A regex like r"c\.(\d+)([ACGT])>([ACGT])" handles the simplest case and fails on everything else:

c.215C>G                     substitution
c.76_78del                   range deletion
c.76_77insT                  insertion between bases
c.-24+5G>A                   intronic, in the 5'UTR
c.*12-8del                   intronic, in the 3'UTR
c.76TG[8]                    a tandem repeat
c.[215C>G;847C>T]            two variants in cis
p.(Arg175His)                a predicted protein change
p.Arg175ProfsTer13           a frameshift with a downstream stop
g.7676154_7676156delinsAC    a delins

The HGVS grammar handles all of these. It also rejects malformed input with a HGVSParseError rather than silently matching a prefix: which is the property that actually matters in a pipeline. A regex that half-matches gives you wrong data with no error.

Error handling

[ ]python
out1ms

This code completed without producing output.

HGVSParseError means the string is not valid HGVS syntax. It says nothing about whether the variant is real: that is hgvs Validator's job. A parse succeeding tells you the string is well-formed, not that base 215 of that transcript is actually a C.

Parsing thousands of clinician-entered strings, expect a 5–20% failure rate from legacy notation, typos, and Excel damage. Log the failures; do not discard them silently.

Parsing sub-components

[3]python
error1ms
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/hgvs/parser.py", line 127, in rule_fxn
    return self._grammar(s).__getattr__(rule_name)()
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/parsley.py", line 98, in invokeRule
    raise err
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/ometa/runtime.py", line 554, in many
    v, _ = fn()
           ~~^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/hgvs/generated/hgvs_grammar.py", line 3250, in _G_many1_894
    _G_apply_895, lastError = self._apply(self.rule_digit, "digit", [])
                              ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/ometa/runtime.py", line 495, in _apply
    [rule(), self.input])
     ~~~~^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/ometa/runtime.py", line 790, in digit
    x, e = self.rule_anything()
           ~~~~~~~~~~~~~~~~~~^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/ometa/runtime.py", line 813, in rule_anything
    h, p = self.input.head()
           ~~~~~~~~~~~~~~~^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/ometa/runtime.py", line 237, in head
    raise EOFError(data, self.position + 1)
ometa.runtime.EOFError: 
c.215
^
Parse error at line 2, column 0: end of input. trail: [p_hgvs_position hgvs_position]


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/leviosman/Developer/python-for-bio/webapp/scripts/curriculum/generate_cell_outputs.py", line 128, in run_cell_capture
    value = eval(compile(tail, "<cell>", "eval"), namespace)
  File "<cell>", line 6, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/hgvs/parser.py", line 129, in rule_fxn
    raise HGVSParseError(
    ...<3 lines>...
    )
hgvs.exceptions.HGVSParseError: c.215: char 6: end of input

Useful when you have the accession in one column and the change in another: a common shape for spreadsheets from clinical labs.

Round-tripping and normalising formatting

[4]python
out1ms
'NM_000546.6:c.215C>G'

Parsing then re-stringifying gives you canonical formatting: consistent spacing, consistent representation. It does not give you canonical coordinates; that requires hgvs Normalizer and sequence data.

[ ]python
out1ms

This code completed without producing output.

The config controls how variants are rendered: for example whether c.76_78delCTT prints the deleted bases or just c.76_78del.

Batch parsing

import pandas as pd
from hgvs.exceptions import HGVSParseError

hp = hgvs.parser.Parser()

def parse_column(series):
    parsed, errors = [], []
    for s in series:
        try:
            parsed.append(hp.parse_hgvs_variant(s))
            errors.append(None)
        except HGVSParseError as e:
            parsed.append(None)
            errors.append(str(e))
    return pd.DataFrame({"variant": parsed, "parse_error": errors},
                        index=series.index)

result = df.join(parse_column(df["hgvs"]))
print(f"{result['parse_error'].notna().sum()} of {len(result)} failed to parse")
print(result.loc[result["parse_error"].notna(), "hgvs"].head(20).tolist())

Printing the failures is the important line: the failure patterns usually reveal one systematic problem (a missing accession prefix, a legacy IVS notation) that you can fix in bulk.

Cleaning before parsing

Common fixable problems in real-world data:

def tidy(s):
    s = str(s).strip()
    s = s.replace("–", "-").replace("—", "-")     # en/em dash → hyphen
    s = s.replace(">", ">")                             # HTML-escaped >
    s = re.sub(r"\s+", "", s)                                # internal whitespace
    if ":" not in s and default_accession:
        s = f"{default_accession}:{s}"                        # add a missing accession
    return s

Word processors substituting en-dashes for hyphens is a genuinely common cause of parse failures in variant lists pasted from documents.

Do not "fix" the semantics. Repairing whitespace and dashes is safe. Rewriting c.76_77insG to c.76dup is a normalization decision that belongs to hgvs Normalizer with sequence data behind it, not to a cleanup regex.

Performance

Parsing is pure computation: no network, no database: and runs at roughly 1,000–10,000 variants per second depending on complexity. It will not be your bottleneck. Projection and normalization, which hit UTA and Data Providers, will be.

Common mistakes

  • Constructing Parser() in a loop.
  • Regex instead of the parser.
  • Assuming a successful parse means a valid variant. Use hgvs Validator.
  • Discarding parse failures instead of logging them.
  • Stripping the accession version during cleanup.
  • Catching bare Exception and masking real bugs. Catch HGVSParseError.
  • Rewriting semantics during cleanup.
  • Expecting round-tripping to normalize coordinates. It normalizes formatting only.

See also

SequenceVariant · HGVS Nomenclature · hgvs Normalizer · hgvs Validator · hgvs easy · String Accessor

Exercise

The hgvs package is not available in this sandbox, so instead of calling Parser.parse_hgvs_variant you will build the small, strict regex parser the lesson argues against: and discover exactly where it stops. The point is not that regex parsing is a good idea; it is that you can only judge the grammar once you have written its cheap imitation.

Write parse_variants(lines), taking the list of input lines and returning a list of output lines. Ignore blank lines. A line is accepted only if it matches <accession>:<type>.<posedit> in full, where the accession is two letters, an underscore, digits, a dot and a version number; the type is one of g, c, n, m; the position is a single position or start_end; and the edit is one of del, dup, ins, inv, delins (each optionally carrying bases) or a substitution X>Y. A substitution must carry a single position and an insertion must carry a range: reject either otherwise. Emit accession|type|start|end|edit_type for an accepted line, using the start position again as the end when only one was given and reporting delins rather than del for a delins. Emit <line>|parse_error for anything else, which includes tandem repeats and phased alleles: they are valid HGVS that this grammar cannot reach. Finish with a line parsed=<n> failed=<m>, which is the only output when the input is empty.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdinNM_000546.6:c.215C>G NM_000546.6:c.76_78del NM_000546.6:c.76_77insT NM_000546.6:c.76_78delinsAC NC_000017.11:g.7676154G>C NM_000546.6:c.-24+5G>A NM_000546.6:c.*12-8del NM_000546.6:c.76_78dup NM_000546.6:c.76_78inv NM_000546.6|c|215|215|sub NM_000546.6|c|76|78|del NM_000546.6|c|76|77|ins NM_000546.6|c|76|78|delins NC_000017.11|g|7676154|7676154|sub NM_000546.6|c|-24+5|-24+5|sub NM_000546.6|c|*12-8|*12-8|del NM_000546.6|c|76|78|dup NM_000546.6|c|76|78|inv parsed=9 failed=0
2stdinc.215C>G NM_000546:c.215C>G NM_000546.6:x.215C>G NM_000546.6:c.215C>G>T NM_000546.6:c.215X>G NM_000546.6:c.215_216C>G NM_000546.6:c.76insT NM_000546.6:c.76TG[8] NM_000546.6:c.[215C>G;847C>T] NM_000546.6:c.215 c.215C>G|parse_error NM_000546:c.215C>G|parse_error NM_000546.6:x.215C>G|parse_error NM_000546.6:c.215C>G>T|parse_error NM_000546.6:c.215X>G|parse_error NM_000546.6:c.215_216C>G|parse_error NM_000546.6:c.76insT|parse_error NM_000546.6:c.76TG[8]|parse_error NM_000546.6:c.[215C>G;847C>T]|parse_error NM_000546.6:c.215|parse_error parsed=0 failed=10
3stdinNM_000546.6:c.76_78delinsAC NM_000546.6:c.76_78delCTT NM_000546.6|c|76|78|delins NM_000546.6|c|76|78|del parsed=2 failed=0
4stdin parsed=0 failed=0

Hints · 3

01Hint

Anchor the match with ^...$ or use re.fullmatch, so a half-match is a rejection rather than a truncated parse.

02Hint

A position is \*?-?\d+(?:[+-]\d+)? — that covers 215, -24, *12, 87+3 and -24+5.

03Hint

Test the edit patterns in order with delins before del, or every delins parses as a deletion.