SequenceVariant
In one line: the object model behind a parsed HGVS string — accession, type, position and edit, all separately addressable.
The structure
SequenceVariant
├─ ac 'NM_000546.6' the accession, with version
├─ type 'c' g | c | n | r | p | m
├─ gene None optional
└─ posedit PosEdit
├─ pos Interval
│ ├─ start BaseOffsetPosition(base=215, offset=0, datum=CDS_START)
│ └─ end BaseOffsetPosition(base=215, offset=0, datum=CDS_START)
├─ edit NARefAlt(ref='C', alt='G')
└─ uncertain False
var = hp.parse_hgvs_variant("NM_000546.6:c.215C>G")
var.ac # 'NM_000546.6'
var.type # 'c'
var.posedit.pos.start.base # 215
var.posedit.pos.start.offset # 0 (non-zero = intronic)
var.posedit.pos.end.base # 215
var.posedit.edit.ref # 'C'
var.posedit.edit.alt # 'G'
var.posedit.edit.type # 'sub'
str(var) # round-trips to the original string
Position classes
| Class | Used by | Fields |
|---|---|---|
SimplePosition |
g., m., n. |
base |
BaseOffsetPosition |
c., r. |
base, offset, datum |
AAPosition |
p. |
base, aa |
Interval |
all | start, end, uncertain |
BaseOffsetPosition is where the complexity lives:
from hgvs.enums import Datum
pos.base # the coding position
pos.offset # 0 = exonic; ±n = intronic
pos.datum # Datum.CDS_START (normal), CDS_END (the * region), SEQ_START
pos.is_intronic # offset != 0
pos.is_uncertain
Worked examples:
| HGVS | base | offset | datum |
|---|---|---|---|
c.215 |
215 | 0 | CDS_START |
c.-24 |
−24 | 0 | CDS_START |
c.87+3 |
87 | +3 | CDS_START |
c.88-3 |
88 | −3 | CDS_START |
c.*12 |
12 | 0 | CDS_END |
c.*12-8 |
12 | −8 | CDS_END |
base alone is not a position. c.87+3 has base 87, and so does c.87; they are 3 bp apart in the genome and in completely different functional contexts (one intronic, one exonic). Any code doing if var.posedit.pos.start.base == 87 without checking the offset is wrong.
Likewise, datum distinguishes c.12 (the 12th coding base) from c.*12 (the 12th base after the stop) — both have base == 12.
Edit classes
| Class | For | Fields |
|---|---|---|
NARefAlt |
sub, del, ins, delins, identity | ref, alt |
Dup |
duplication | ref |
Inv |
inversion | ref |
Repeat |
tandem repeats | ref, min, max |
AARefAlt |
protein sub/del/ins | ref, alt |
AAFs |
frameshift | ref, alt, length |
AAExt |
extension | |
AASub |
protein substitution |
var.posedit.edit.type # 'sub' | 'del' | 'ins' | 'delins' | 'dup' | 'inv' | 'identity'
Dispatching on edit.type is how you write code that handles all variant classes:
def describe(var):
e = var.posedit.edit
match e.type:
case "sub": return f"{e.ref}>{e.alt}"
case "del": return f"deletion of {len(e.ref or '')} bp"
case "ins": return f"insertion of {e.alt}"
case "delins": return f"{len(e.ref or '')} bp → {e.alt}"
case "dup": return "duplication"
case "inv": return "inversion"
case _: return e.type
ref is often None. HGVS permits c.76_78del without spelling out the deleted bases. hgvs fills it in from the sequence during normalization — one of the reasons to normalize before analysing. Guard with e.ref or "".
Constructing variants programmatically
import hgvs.edit, hgvs.location, hgvs.posedit, hgvs.sequencevariant
from hgvs.enums import Datum
pos = hgvs.location.BaseOffsetPosition(base=215, offset=0, datum=Datum.CDS_START)
iv = hgvs.location.Interval(start=pos, end=pos)
edit = hgvs.edit.NARefAlt(ref="C", alt="G")
var = hgvs.sequencevariant.SequenceVariant(
ac="NM_000546.6", type="c",
posedit=hgvs.posedit.PosEdit(pos=iv, edit=edit),
)
str(var) # 'NM_000546.6:c.215C>G'
Verbose, but it is how you build variants from structured data (a VCF row, a database record) without string formatting. Building a variant by string concatenation and then parsing it is fragile — you can easily produce something syntactically valid and semantically wrong (an ins that should be a dup).
Copying and mutating
SequenceVariant objects are effectively value objects. To modify one, copy it:
import copy
v2 = copy.deepcopy(var)
v2.ac = "NM_000546.5"
Mutating in place risks aliasing bugs if you have kept references. Most hgvs operations (normalize, c_to_g) return new objects rather than mutating, which is the right design.
Extracting to a DataFrame
The bridge into the pandas half of your analysis:
def to_row(var):
pe = var.posedit
return {
"accession": var.ac,
"type": var.type,
"start_base": pe.pos.start.base,
"start_offset": getattr(pe.pos.start, "offset", 0),
"end_base": pe.pos.end.base,
"end_offset": getattr(pe.pos.end, "offset", 0),
"edit_type": pe.edit.type,
"ref": getattr(pe.edit, "ref", None),
"alt": getattr(pe.edit, "alt", None),
"is_intronic": getattr(pe.pos.start, "offset", 0) != 0,
"hgvs": str(var),
}
df = pd.DataFrame([to_row(v) for v in variants])
getattr(..., default) throughout, because g. variants use SimplePosition (no offset) and Dup/Inv edits have no alt. Writing pe.pos.start.offset unconditionally crashes on genomic variants.
Comparison and equality
v1 == v2 # structural equality
str(v1) == str(v2) # string equality
Neither is biological equivalence. c.76_78del and c.77_79del can be the same event in a repeat, and they compare unequal. To compare variants meaningfully:
- Normalize both (hgvs Normalizer).
- Project both to the same reference type and accession (AssemblyMapper).
- Then compare.
Skipping either step gives you false negatives. This is the single most consequential thing to understand about working with variants. See hgvs Pitfalls.
Common mistakes
- Using
basewithoutoffset.c.87+3≠c.87. - Ignoring
datum.c.12≠c.*12. - Assuming
refis populated. Normalize first, or guard withor "". - Assuming
altexists ondupandinvedits. pos.start.offseton ag.variant → AttributeError.- Comparing unnormalized variants and concluding they differ.
- String-building variants instead of constructing objects.
- Stripping the accession version.
See also
hgvs Parser · HGVS Nomenclature · hgvs Normalizer · Projection c to g to p · Coordinate Systems · hgvs Pitfalls