Skip to content

Gene identifiers

One gene carries many names: an Ensembl stable id, an NCBI Entrez GeneID, a UniProt accession, an HGNC id, and the symbol a paper prints. An xref set is one publisher's table of which of those names go together, for one species, at one pinned release. Name a species and the set is prepared on this machine, then ask it in either direction.

from genome.xref import XrefSet

human = XrefSet("Homo sapiens")
human.to_stems(["7157"], "entrez").gene_id_stems    # ['ENSG00000141510']

No genome is opened and no assembly is named. What a gene is called in another species is a separate question, answered on Homology.

Preparing a set

The first XrefSet("Homo sapiens") downloads the publisher's file, slices it to that one species and writes the slice as a plain gzipped TSV. Every construction after reads the slice and fetches nothing.

The files land in the shared data directory, where every project on the machine reads the same copy.

The first use downloads. The lab's CPU compute nodes have no internet, so run genome xref ids once on a login node before submitting a job that needs it.

A set prints which species, source and release it holds, and how many genes it covers. namespaces is read off the slice, so it lists what this set can actually answer in:

human
# XrefSet(species='Homo sapiens', source='alliance', release='9.0.0', stems=43867)

human.namespaces    # ('ensembl', 'entrez', 'uniprot', 'hgnc')

Each species has its own authority, so a human set carries hgnc, a mouse set mgi and a worm set wormbase. Asking for one the set does not carry raises NamespaceNotCarriedError, and the message lists the ones it does.

Sources and releases

Three species have a set, and more than one publisher offers each of them:

from genome.xref import xref_releases, xref_sources, xref_species

xref_species()
# ('Homo sapiens', 'Mus musculus', 'Caenorhabditis elegans')

xref_sources("Homo sapiens")                 # ('alliance', 'ensembl', 'hgnc')
xref_releases("Homo sapiens", "alliance")    # ('9.0.0',)

Leave the source out and the species' default answers, so everyone in the lab reaches for the same one. The default is per species and per question, because the publisher that carries a species' ids is usually not the one that carries its symbols. lookup_xref says which row will answer, without downloading anything:

from genome.xref import lookup_xref

lookup_xref("Homo sapiens").source                      # 'alliance'
lookup_xref("Homo sapiens", for_symbols=True).source    # 'hgnc'
lookup_xref("Mus musculus", for_symbols=True).source    # 'alliance_bgi'

Name a source, as XrefSet("Homo sapiens", "ensembl"), and it is honoured rather than swapped for a default. Two publishers can disagree about which Ensembl gene an Entrez id names, so pick one deliberately when it matters; every answer records the source and release that produced it. Naming a species, source or release nothing is prepared for raises NoXrefSetError.

There is more here than the common path uses: an evidence= filter for publishers that grade their rows, the normalise_id and normalise_symbol helpers, and a reader module per publisher. All of it is in the API reference.

Converting ids

Everything is joined through one namespace. A gene id stem is an Ensembl gene id with any version suffix removed, and it is what both directions have in common. to_stems reads ids in a namespace and answers in stems; from_stems reads stems and answers in a namespace. Which one you want is named rather than guessed from the string:

human.to_stems(["7157", "672"], "entrez").resolved
# {'7157': ('ENSG00000141510',), '672': ('ENSG00000012048',)}

human.from_stems(["ENSG00000141510"], "uniprot").resolved
# {'ENSG00000141510': ('P04637',)}

The keys are your own spelling of what you asked about, in the order you asked, so the answer lines up against your table row for row. A value is every gene the publisher asserts, never one picked for you. An HGNC id that names two genes answers with both:

human.to_stems(["HGNC:11998", "HGNC:13666"], "hgnc").resolved
# {'HGNC:11998': ('ENSG00000141510',),
#  'HGNC:13666': ('ENSG00000094914', 'ENSG00000291836')}

gene_id_stems flattens those values into one list. It drops which id named which gene, so read resolved whenever that matters. There is no direct hop from one namespace to another: Entrez to UniProt is two calls with the stem in the middle, and you join them.

What did not resolve

Ids this release names nothing for ride back on the answer instead of being dropped:

answer = human.to_stems(["7157", "999999999"], "entrez")
answer.resolved      # {'7157': ('ENSG00000141510',)}
answer.unresolved    # ('999999999',)

Read unresolved on every answer, because a hand-written join drops these rows without saying so. It is in ask order, and a stem that carries no id in the namespace you asked for lands there too. Nothing here separates an id a publisher retired from one it never carried.

Versioned ids and stems

GENCODE and Ensembl's own GTFs spell a gene id with a version suffix, ENSG00000141510.18. Most cross-reference files spell it bare. Joining a versioned column to a bare one matches nothing and raises no error, which is the usual cause of an empty lookup with nothing to explain it.

The version is dropped on both sides here, so the two spellings are one identifier:

human.from_stems(["ENSG00000141510.18", "ENSG00000141510"], "uniprot").resolved
# {'ENSG00000141510.18': ('P04637',), 'ENSG00000141510': ('P04637',)}

Two keys, because the keys are your spelling, and the same value under both. For a column you are cleaning up before a join of your own, gene_id_stem does the reduction on its own:

from genome.xref import gene_id_stem

gene_id_stem("ENSG00000141510.18")    # 'ENSG00000141510'

Worm ids are published unversioned, so WBGene00003001 is already a stem and the reduction changes nothing.

Matching symbols

The two directions are different questions. Starting from a stem, a gene has one current approved symbol, which is what a figure axis wants, and from_stems gives it:

symbols = XrefSet.for_symbols("Homo sapiens")
symbols.from_stems(["ENSG00000133794"], "symbol").resolved    # {'ENSG00000133794': ('BMAL1',)}

Starting from a symbol, the answer is genes, and the match is made against spellings the authority has since retired as well as the current one. That is match_symbols, and every hit says which kind of spelling matched:

answer = symbols.match_symbols(["ARNTL", "ADCY3", "Brca1"])
answer.resolved
# {'ARNTL': (SymbolMatch(symbol='ARNTL', gene_id_stem='ENSG00000133794', kind='previous'),),
#  'ADCY3': (SymbolMatch(symbol='ADCY3', gene_id_stem='ENSG00000138031', kind='approved'),
#            SymbolMatch(symbol='ADCY3', gene_id_stem='ENSG00000155897', kind='previous'))}
answer.unresolved    # ('Brca1',)

ARNTL is a spelling HGNC retired and it still reaches BMAL1. ADCY3 is HGNC's approved symbol for one gene and a symbol it took away from another, so it answers with both. Matching is exact by default, so Brca1, a mouse spelling asked of the human authority, matches nothing. Pass case_insensitive=True to fold both sides.

XrefSet.for_symbols fills in the source that carries symbols; the plain constructor fills in the identifier one, which for human carries no symbol at all, so XrefSet("Homo sapiens").match_symbols(...) raises NamespaceNotCarriedError. Asking to_stems for the symbol namespace raises SymbolDirectionError, whose message names match_symbols.

What a source could not match

Only HGNC labels its previous and alias spellings as such. The mouse and worm authorities each publish one approved symbol per gene beside an undifferentiated synonyms list, so those sets match approved spellings and nothing else. The pair that works for human does not work for mouse:

mouse = XrefSet.for_symbols("Mus musculus")
answer = mouse.match_symbols(["Bmal1", "Arntl"])
answer.resolved
# {'Bmal1': (SymbolMatch(symbol='Bmal1', gene_id_stem='ENSMUSG00000055116', kind='approved'),)}
answer.unresolved    # ('Arntl',)
answer.kinds         # ('approved',)

A symbol this release does not have and a spelling this source could never have matched both land in unresolved, and the entry alone does not say which happened. kinds says which kinds the set can match, and limits says why the rest are missing. limits is a long string, trimmed here after its first clause:

print(answer.limits)
# this source publishes one current approved symbol per gene beside an undifferentiated
# synonyms list, and a spelling in that list does not say whether the authority retired
# it or merely records it ...

For a human symbol set, kinds is ('approved', 'previous', 'alias') and limits is None.

Citing what answered

A set carries the curated row it resolved to, so what you cite is what answered rather than what you looked up afterwards:

print(symbols.provenance.attribution())
# HGNC 2026-07-07 (PMID 41287213) —
# https://storage.googleapis.com/public-download-files/hgnc/archive/archive/quarterly/tsv/hgnc_complete_set_2026-07-07.txt

Publisher, their own version string, the paper to cite where there is one, and the file the bytes came from. provenance is the whole record, including the species, the NCBI taxid and the publisher's checksum. Each command below prints its own source URL on every answer.

From the command line

Neither question needs a genome, and both have a command. genome xref ids converts in the direction the flag names:

$ genome xref ids "Homo sapiens" --to-stems entrez 7157 672 999999999
entrez ids -> gene id stems for Homo sapiens (alliance 9.0.0)
  source  https://download.alliancegenome.org/9.0.0/GENECROSSREFERENCE/COMBINED/GENECROSSREFERENCE_COMBINED_11.tsv.gz
  2 resolved, 2 gene id stems, 1 this release names none for
7157 ENSG00000141510
672 ENSG00000012048
999999999

The pairs go to stdout, tab-separated, so cut -f2 is the answer and the rest pipes. The heading and the counts go to stderr. Every id you passed gets a row, and one that resolved to nothing gets an empty second column.

genome xref symbols matches, and prints the kind of spelling in the fourth column:

$ genome xref symbols "Homo sapiens" ARNTL ADCY3 Brca1
gene symbols -> gene id stems for Homo sapiens (hgnc 2026-07-07)
  source   https://storage.googleapis.com/public-download-files/hgnc/archive/archive/quarterly/tsv/hgnc_complete_set_2026-07-07.txt
  columns  asked, symbol, gene_id_stem, kind
  matching exact, on approved, previous, alias spellings
  2 resolved, 3 matches, 1 this release matched nothing for
ARNTL ARNTL ENSG00000133794 previous
ADCY3 ADCY3 ENSG00000138031 approved
ADCY3 ADCY3 ENSG00000155897 previous
Brca1

Both take --source and --json. Both exit non-zero when a species, source or namespace is not one this package has, rather than printing an answer shorter than you asked for. The full option list is on CLI: lookup commands.