ChEMBL, offline: CheMBL¶
ChEMBL (EMBL-EBI) is a curated database of bioactive, drug-like molecules.
CheMBL reads a local copy of it: compound records, names and synonyms,
structures, and computed properties.
What is installed. A full ChEMBL release is a 27.7 GiB SQLite file with 74
tables, and PROVESID reads eight of them. datasets.fetch("chembl") downloads
the release (5.8 GB) and keeps only an extract of those eight tables
(2.4 GiB). The bioactivity tables (assays, activities, targets) are not in the
extract. CheMBL(source="full") keeps the whole release, if you need them
through your own SQL. Installing the offline databases
describes the routes.
The outputs below were produced on 2026-09-23 with the ChEMBL 36 extract.
import pandas as pd
from provesid import CheMBL
pd.set_option("display.width", 200)
pd.set_option("display.max_colwidth", 50)
chembl = CheMBL()
chembl.release, chembl.is_compact, chembl.db_path
(36, True, '/home/ali/.local/share/provesid/chembl_36_provesid.db')
By ChEMBL ID¶
aspirin = chembl.search_by_chembl_id("CHEMBL25")
{key: aspirin[key] for key in ("chembl_id", "molregno", "pref_name", "max_phase",
"canonical_smiles", "standard_inchi_key")}
{'chembl_id': 'CHEMBL25',
'molregno': 1280,
'pref_name': 'ASPIRIN',
'max_phase': 4,
'canonical_smiles': 'CC(=O)Oc1ccccc1C(=O)O',
'standard_inchi_key': 'BSYNRYMUTXBXSQ-UHFFFAOYSA-N'}
synonyms lists every synonym row, one per synonym type, so the same name can
appear several times:
print(len(aspirin["synonyms"]), "rows,", len(set(aspirin["synonyms"])), "distinct")
sorted(set(aspirin["synonyms"]))[:12]
80 rows, 75 distinct
['8-hour bayer', 'Acetosalic acid', 'Acetyl salicylate', 'Acetylsalic acid', 'Acetylsalicylic acid', 'Acetylsalicylic acid (who-ip)', 'Acetylsalicylicum acidum', 'Acidum acetylsalicylicum', 'Acidum acetylsalicylicum (who-ip)', 'Alka rapid', 'Anadin all night', 'Angettes 75']
By name¶
search_by_name matches the preferred name and the synonyms, ignoring case.
By default it matches any name that contains the text, so the results
include salts, mixtures and derivatives:
pd.DataFrame(chembl.search_by_name("caffeine"))[["chembl_id", "pref_name", "canonical_smiles"]]
| chembl_id | pref_name | canonical_smiles | |
|---|---|---|---|
| 0 | CHEMBL113 | CAFFEINE | Cn1c(=O)c2c(ncn2C)n(C)c1=O |
| 1 | CHEMBL70246 | ACEFYLLINE | Cn1c(=O)c2c(ncn2CC(=O)O)n(C)c1=O |
| 2 | CHEMBL1200569 | CAFFEINE CITRATE | Cn1c(=O)c2c(ncn2C)n(C)c1=O.O=C(O)CC(O)(CC(=O)O... |
| 3 | CHEMBL2088312 | NaN | Cn1c(=O)c2c(nc(CSc3cccc(F)c3)n2C)n(C)c1=O |
exact=True matches whole names only:
pd.DataFrame(chembl.search_by_name("caffeine", exact=True))[["chembl_id", "pref_name"]]
| chembl_id | pref_name | |
|---|---|---|
| 0 | CHEMBL113 | CAFFEINE |
A substring search on a short word finds compounds whose synonyms contain it, even when the preferred name does not:
pd.DataFrame(chembl.search_by_name("acetyl", limit=8))[["chembl_id", "pref_name"]]
| chembl_id | pref_name | |
|---|---|---|
| 0 | CHEMBL25 | ASPIRIN |
| 1 | CHEMBL443 | SULFAMETHOXAZOLE |
| 2 | CHEMBL268834 | ACETYLROTENOLENE |
| 3 | CHEMBL45 | MELATONIN |
| 4 | CHEMBL9080 | MERCAPTOACETYLTRIGLYCINE |
| 5 | CHEMBL403516 | ACETYLSHIKONIN |
| 6 | CHEMBL539 | ACETIC ACID |
| 7 | CHEMBL112 | ACETAMINOPHEN |
By structure¶
All three lookups are exact string matches. For SMILES that means ChEMBL's own canonical form, character for character. The aspirin SMILES below matches because it is written as ChEMBL writes it. When in doubt, look up the InChIKey:
for label, record in [
("SMILES", chembl.search_by_smiles("CC(=O)Oc1ccccc1C(=O)O")),
("InChIKey", chembl.search_by_inchikey("BSYNRYMUTXBXSQ-UHFFFAOYSA-N")),
("InChI", chembl.search_by_inchi(
"InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)")),
]:
print(f"{label:9} -> {record['chembl_id'] if record else None} "
f"{record['pref_name'] if record else ''}")
SMILES -> CHEMBL25 ASPIRIN InChIKey -> CHEMBL25 ASPIRIN InChI -> CHEMBL25 ASPIRIN
Computed properties¶
get_properties takes the internal molregno, which every record carries:
props = chembl.get_properties(aspirin["molregno"])
{key: props[key] for key in ("mw_freebase", "alogp", "hba", "hbd", "psa", "rtb",
"aromatic_rings", "heavy_atoms", "num_ro5_violations")}
{'mw_freebase': 180.16,
'alogp': 1.31,
'hba': 3,
'hbd': 1,
'psa': 63.6,
'rtb': 2,
'aromatic_rings': 1,
'heavy_atoms': 13,
'num_ro5_violations': 0}
alogp is ChEMBL's calculated logP. It is not a measurement.
chembl_id_to_molregno and molregno_to_chembl_id convert between the two
IDs:
chembl.chembl_id_to_molregno("CHEMBL25"), chembl.molregno_to_chembl_id(1280)
(1280, 'CHEMBL25')
A small table¶
rows = []
for chembl_id in ["CHEMBL25", "CHEMBL521", "CHEMBL112", "CHEMBL113"]:
record = chembl.search_by_chembl_id(chembl_id)
props = chembl.get_properties(record["molregno"])
rows.append({"chembl_id": chembl_id, "name": record["pref_name"],
"MW": props["mw_freebase"], "ALogP": props["alogp"],
"HBA": props["hba"], "HBD": props["hbd"], "PSA": props["psa"],
"Ro5 violations": props["num_ro5_violations"]})
pd.DataFrame(rows)
| chembl_id | name | MW | ALogP | HBA | HBD | PSA | Ro5 violations | |
|---|---|---|---|---|---|---|---|---|
| 0 | CHEMBL25 | ASPIRIN | 180.16 | 1.31 | 3 | 1 | 63.60 | 0 |
| 1 | CHEMBL521 | IBUPROFEN | 206.28 | 3.07 | 1 | 1 | 37.30 | 0 |
| 2 | CHEMBL112 | ACETAMINOPHEN | 151.16 | 1.35 | 2 | 2 | 49.33 | 0 |
| 3 | CHEMBL113 | CAFFEINE | 194.19 | -1.03 | 6 | 0 | 61.82 | 0 |
Closing, and threads¶
CheMBL holds an SQLite connection. Close it when you are done, or use it as
a context manager. One instance may be shared between threads. See
Using the local databases directly.
chembl.close()
with CheMBL() as db:
print(db.search_by_chembl_id("CHEMBL113")["pref_name"])
CAFFEINE
Search uses CheMBL as one of its sources; see the
Search tutorial.