ChEBI web service¶
ChEBI (Chemical Entities of Biological Interest, EMBL-EBI) is a curated
dictionary of small molecules with an ontology of chemical classes and
biological roles. The ChEBI class is a client for its 2.0 REST API.
Every call here needs the network. For names, structures and CAS numbers
without it, use the downloaded ChEBI file through ChebiSDF (the
ChEBI SDF tutorial). The web service adds what
the file does not carry: the ontology, roles, citations, and structure
search. A failed lookup returns None and logs a warning. The client never
raises.
The outputs below were produced on 2026-09-23.
import pandas as pd
from provesid import ChEBI
pd.set_option("display.width", 200)
pd.set_option("display.max_colwidth", 60)
chebi = ChEBI()
A compound record¶
get_compound takes a ChEBI ID with or without the CHEBI: prefix:
caffeine = chebi.get_compound(27732)
list(caffeine)
['id', 'chebi_accession', 'name', 'stars', 'definition', 'ascii_name', 'names', 'chemical_data', 'modified_on', 'secondary_ids', 'default_structure', 'compound_origins', 'created_by', 'ontology_relations', 'database_accessions', 'roles_classification', 'is_released']
The structure, the formula and the mass are in two nested blocks:
structure = caffeine["default_structure"]
chemistry = caffeine["chemical_data"]
print("name: ", caffeine["ascii_name"], f"({caffeine['stars']} stars)")
print("formula: ", chemistry["formula"], " mass:", chemistry["mass"])
print("SMILES: ", structure["smiles"])
print("InChIKey:", structure["standard_inchi_key"])
name: caffeine (3 stars) formula: C8H10N4O2 mass: 194.194 SMILES: Cn1c(=O)c2c(ncn2C)n(C)c1=O InChIKey: RYYVLZVUVIJVGH-UHFFFAOYSA-N
name can hold HTML (caffeine-<em>d</em><small><sub>9</sub></small>), so
ascii_name is the one to print or store.
Names and cross-references¶
names groups the names by type, and each entry says where it came from:
{kind: len(entries) for kind, entries in caffeine["names"].items()}
{'SYNONYM': 28, 'IUPAC NAME': 1, 'UNIPROT NAME': 1, 'BRAND NAME': 26}
pd.DataFrame(caffeine["names"]["SYNONYM"])[["ascii_name", "source", "language_code"]].head(8)
| ascii_name | source | language_code | |
|---|---|---|---|
| 0 | 1,3,7-trimethyl-2,6-dioxopurine | ChemIDplus | en |
| 1 | 1,3,7-trimethylpurine-2,6-dione | IUPHAR | en |
| 2 | 1,3,7-trimethylxanthine | NIST Chemistry WebBook | en |
| 3 | 1,3,7-Trimethylxanthine | KEGG COMPOUND | en |
| 4 | 1-methyltheobromine | ChemIDplus | en |
| 5 | 3,7-Dihydro-1,3,7-trimethyl-1H-purin-2,6-dion | NIST Chemistry WebBook | de |
| 6 | 7-methyltheophylline | NIST Chemistry WebBook | en |
| 7 | anhydrous caffeine | KEGG DRUG | en |
database_accessions does the same for identifiers in other databases,
including CAS numbers and the literature:
{kind: len(entries) for kind, entries in caffeine["database_accessions"].items()}
{'CITATION': 117, 'REGISTRY_NUMBER': 2, 'MANUAL_X_REF': 10, 'CAS': 3}
sorted({entry["accession_number"] for entry in caffeine["database_accessions"]["CAS"]})
['58-08-2']
Roles and the ontology¶
roles_classification lists the chemical and biological roles ChEBI assigns:
[role["name"] for role in caffeine["roles_classification"]]
['adenosine A2A receptor antagonist', 'mutagen', 'antineoplastic agent', 'mouse metabolite', 'ryanodine receptor agonist', 'immunosuppressive agent', 'hypoglycemic agent', 'xenobiotic', 'analgesic', 'anti-arrhythmia drug', 'antirheumatic drug', 'adenosine receptor antagonist', 'plant metabolite', 'antiviral agent', 'anti-inflammatory agent', 'antipsoriatic', 'anti-obesity agent', 'cardiovascular drug', 'psychotropic drug', 'antiparkinson drug', 'EC 3.1.4.* (phosphoric diester hydrolase) inhibitor', 'fungal metabolite', 'human blood serum metabolite', 'EC 2.7.11.1 (non-specific serine/threonine protein kinase) inhibitor', 'anti-HIV agent', 'geroprotector', 'anti-anaemic agent', 'central nervous system stimulant', 'environmental contaminant', 'dermatologic drug', 'vasoconstrictor agent', 'food additive', 'diuretic', 'adjuvant', 'metabolite']
get_ontology_parents returns the relations from a compound (is a,
has role, has functional parent, …), and get_ontology_children returns
the relations to it. Both come back under ontology_relations:
parents = chebi.get_ontology_parents(16236) # ethanol
pd.DataFrame(parents["ontology_relations"]["outgoing_relations"])[
["relation_type", "final_name", "final_id"]]
| relation_type | final_name | final_id | |
|---|---|---|---|
| 0 | has role | disinfectant | 48219 |
| 1 | has role | teratogenic agent | 50905 |
| 2 | has role | NMDA receptor antagonist | 60643 |
| 3 | has role | protein kinase C agonist | 64018 |
| 4 | has role | central nervous system depressant | 35488 |
| 5 | has role | <em>Escherichia coli</em> metabolite | 76971 |
| 6 | has role | neurotoxin | 50910 |
| 7 | has role | antiseptic drug | 48218 |
| 8 | has role | polar solvent | 48354 |
| 9 | has role | human metabolite | 77746 |
| 10 | has role | <em>Saccharomyces cerevisiae </em> metabolite | 75772 |
| 11 | has role | mouse metabolite | 75771 |
| 12 | is a | primary alcohol | 15734 |
| 13 | is a | alkyl alcohol | 50584 |
| 14 | is a | volatile organic compound | 134179 |
| 15 | is a | ethanols | 23982 |
| 16 | is conjugate acid of | ethoxide | 52092 |
children = chebi.get_ontology_children(30879) # alcohol
subclasses = [r["init_name"] for r in children["ontology_relations"]["incoming_relations"]
if r["relation_type"] == "is a"]
print(len(subclasses), "direct subclasses of 'alcohol', for example:")
subclasses[:10]
86 direct subclasses of 'alcohol', for example:
['amino alcohol', 'aromatic alcohol', 'fatty alcohol', 'primary alcohol', 'secondary alcohol', 'tertiary alcohol', 'cyclohexanols', 'hemiacetal', 'prenols', 'hydroxyether']
Searching¶
search_by_name searches names, synonyms and definitions. Each hit carries a
relevance score and the record under _source:
hits = chebi.search_by_name("caffeine", size=5)
pd.DataFrame([{"chebi_id": h["_source"]["chebi_accession"],
"name": h["_source"]["ascii_name"],
"formula": h["_source"].get("formula"),
"score": round(h["_score"], 1)} for h in hits])
| chebi_id | name | formula | score | |
|---|---|---|---|---|
| 0 | CHEBI:27732 | caffeine | C8H10N4O2 | 64.5 |
| 1 | CHEBI:749278 | caffeine citrate | C6H8O7.C8H10N4O2 | 59.2 |
| 2 | CHEBI:32140 | sodium caffeine benzoate | C7H5O2.C8H10N4O2.Na | 57.1 |
| 3 | CHEBI:177330 | caffeine-d9 | C17H28N4O2 | 52.6 |
| 4 | CHEBI:178066 | caffeine-(trimethyl-(13)C3) | C5[13C3]H10N4O2 | 47.8 |
structure_search finds compounds by structure. It supports connectivity,
substructure and similarity searches. A similarity search needs a threshold:
similar = chebi.structure_search("Cn1c(=O)c2c(ncn2C)n(C)c1=O", "similarity",
similarity=0.8, size=8)
[h["_source"]["ascii_name"] for h in similar["results"]]
['caffeine-(trimethyl-(13)C3)', 'caffeine', 'caffeine monohydrate']
Structures¶
get_molfile returns the molfile of a compound's default structure, and
get_compound_structure returns an SVG drawing:
print(chebi.get_molfile(16236))
ChEBI
Marvin 10060515352D
3 2 0 0 0 0 999 V2000
4.8667 -3.3230 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
5.5812 -2.9105 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
6.2956 -3.3230 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0 0 0 0
2 3 1 0 0 0 0
M END
Several compounds at once¶
get_compounds asks for a list in one request. Each entry says whether the ID
exists and whether it is a primary or a secondary (merged) ID:
batch = chebi.get_compounds([15377, 16236, 5418, 999999999])
pd.DataFrame([{"asked": key, "exists": value["exists"], "id_type": value["id_type"],
"primary": value.get("primary_chebi_id"),
"name": (value.get("data") or {}).get("ascii_name")}
for key, value in batch.items()])
| asked | exists | id_type | primary | name | |
|---|---|---|---|---|---|
| 0 | CHEBI:15377 | True | PRIMARY_ID | CHEBI:15377 | water |
| 1 | CHEBI:16236 | True | PRIMARY_ID | CHEBI:16236 | ethanol |
| 2 | CHEBI:5418 | True | SECONDARY_ID | CHEBI:17234 | glucose |
| 3 | CHEBI:999999999 | False | NaN | NaN | NaN |
CHEBI:5418 is a secondary ID that was merged into glucose, CHEBI:17234. Glucose in ChEBI 2.0 is a class with no single structure, which is also why it is missing from the SDF file.
When a lookup fails¶
A missing compound and a failed request both come back as None, with the
reason in the log:
print(chebi.get_compound(999999999))
Failed to get compound CHEBI:999999999: No data for https://www.ebi.ac.uk/chebi/backend/api/public/compound/CHEBI:999999999/ (HTTP 404)
None
The client's exceptions, ChEBINotFoundError and ChEBITimeoutError, are
subclasses of ChEBIError, which derives from provesid.http.ServiceError.
They are raised by the transport and caught by the public methods, so you
only meet them if you call the transport yourself. How requests are paced and
retried is described in Network behaviour.