PubChem: the local database and PUG-REST¶
PROVESID reaches PubChem in three ways:
PubChemID, a local SQLite database of PubChem's identifiers: CID, CAS numbers, names, SMILES, InChI and InChIKey, plus a few computed properties. It answers from disk, with no network.PubChemAPI, a client for PubChem's PUG-REST web service, for everything the local database does not hold: synonyms, substances, most computed properties, and structure search.PubChemView, for the experimental data (melting points, vapour pressures, …). It has its own tutorial, Experimental properties from PubChem View.
This tutorial starts offline and goes online where it has to. The outputs below were produced on 2026-09-23. The online cells need the network.
import pandas as pd
from provesid import PubChemID
from provesid.pubchem import PubChemAPI, CompoundProperties, Domain
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 12)
pd.set_option("display.max_colwidth", 40)
The local database¶
PubChemID opens the installed database (2.2 GiB). If it is not installed,
Installing the offline databases shows how.
db = PubChemID()
record = db.get_by_cas("50-78-2")
{key: record[key] for key in ("cid", "cmpdname", "mf", "mw", "smiles", "inchikey")}
{'cid': 2244,
'cmpdname': 'Aspirin',
'mf': 'C9H8O4',
'mw': 180.16,
'smiles': 'CC(=O)OC1=CC=CC=C1C(=O)O',
'inchikey': 'BSYNRYMUTXBXSQ-UHFFFAOYSA-N'}
A record also carries the CAS numbers and the synonyms PubChem lists:
record["cas_numbers"], record["synonyms"][:8]
(['50-78-2'], ['aspirin', 'ACETYLSALICYLIC ACID', '2-Acetoxybenzoic acid', '2-(Acetyloxy)benzoic acid', 'Acylpyrin', 'o-Acetoxybenzoic acid', 'O-Acetylsalicylic acid', 'Acenterine'])
The conversions come in singular and batch forms:
db.cas_to_cid("58-08-2"), db.cid_to_inchikey(2519), db.inchikey_to_cas("RYYVLZVUVIJVGH-UHFFFAOYSA-N")
(2519, 'RYYVLZVUVIJVGH-UHFFFAOYSA-N', ['58-08-2', '95789-13-2', '001-02-7', '001-03-5', '001-07-6', '001-16-7', '114303-55-8'])
db.batch_cas_to_cid(["64-17-5", "67-56-1", "71-43-2", "0000-00-0"])
{'64-17-5': 702, '67-56-1': 887, '71-43-2': 241, '0000-00-0': None}
One compound can have several CAS numbers, and PubChem does not rank them. For ibuprofen (CID 3672) the usual number, 15687-27-1, is not the first one listed:
db.cid_to_cas(3672)
['58560-75-1', '15687-27-1', '1189866-35-0', '001-02-0', '001-12-9', '001-23-6', '001-24-4']
Properties, offline first¶
properties() answers from disk when it can and asks PUG-REST when it cannot.
Source says which one answered:
db.properties(2244)
{'CID': 2244,
'Source': 'offline',
'MolecularFormula': 'C9H8O4',
'MolecularWeight': 180.16,
'SMILES': 'CC(=O)OC1=CC=CC=C1C(=O)O',
'InChI': 'InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)',
'InChIKey': 'BSYNRYMUTXBXSQ-UHFFFAOYSA-N',
'IUPACName': '2-acetyloxybenzoic acid',
'Title': 'Aspirin',
'ExactMass': 180.042252}
XLogP and TPSA are the output of PubChem's models and are not stored
locally, so asking for them goes online:
db.properties(2244, ["MolecularWeight", "XLogP", "TPSA"])
{'CID': 2244,
'Source': 'online',
'MolecularWeight': 180.16,
'XLogP': 1.2,
'TPSA': 63.6}
To stay offline, turn the fallback off. A request the local database cannot
answer in full then returns None, never a partial answer:
print(db.properties(2244, ["XLogP"], use_online_fallback=False))
None
properties_table does the same for many CIDs, as a DataFrame:
db.properties_table([2244, 2519, 3672])[["CID", "Source", "Title", "MolecularFormula",
"MolecularWeight", "InChIKey"]]
| CID | Source | Title | MolecularFormula | MolecularWeight | InChIKey | |
|---|---|---|---|---|---|---|
| 0 | 2244 | offline | Aspirin | C9H8O4 | 180.16 | BSYNRYMUTXBXSQ-UHFFFAOYSA-N |
| 1 | 2519 | offline | Caffeine | C8H10N4O2 | 194.19 | RYYVLZVUVIJVGH-UHFFFAOYSA-N |
| 2 | 3672 | offline | Ibuprofen, (+-)- | C13H18O2 | 206.28 | HEFNNWSXXWATRW-UHFFFAOYSA-N |
descriptors() computes descriptors with RDKit from the stored structure,
with no network. The logP is RDKit's model, and it is named MolLogP so it
cannot be mistaken for PubChem's XLogP:
db.descriptors(2244, ["MolLogP", "TPSA", "HBondDonorCount", "HBondAcceptorCount"])
{'CID': 2244,
'Source': 'rdkit',
'MolLogP': 1.3101,
'TPSA': 63.6,
'HBondDonorCount': 1,
'HBondAcceptorCount': 3}
db.close()
PUG-REST: names to CIDs¶
pc = PubChemAPI()
pc.get_cids_by_name("caffeine")
[2519]
A name search matches whole names by default. name_type="word" matches any
name that contains the word, which returns hundreds of CIDs, and the first
of them is not necessarily the compound you asked for:
word = pc.get_cids_by_name("caffeine", name_type="word")
len(word), word[:5]
(230, [9871508, 56841593, 3081207, 2519, 145904])
Some identifiers, many CAS numbers of mixtures and natural products among them, are known only to PubChem's substance database. Garlic oil, 8000-78-0, is one:
found = pc.find_cids_comprehensive("8000-78-0")
found["compound_domain"]["cids"], found["substance_domain"]["cids"]
([], [6850738])
Synonyms and identifiers¶
pc.get_compound_synonyms(2519)[:8]
['caffeine', '58-08-2', 'Guaranine', '1,3,7-Trimethylxanthine', 'Methyltheobromine', 'Theine', 'Thein', 'Cafeina']
get_compound_identifiers picks the registry numbers out of the synonym
list:
ids = pc.get_compound_identifiers(2519)
{key: ids[key] for key in ("casrn", "dtxsid", "chebi_id", "chembl", "nsc")}
{'casrn': ['58-08-2',
'001-02-7',
'001-03-5',
'001-07-6',
'001-16-7',
'114303-55-8'],
'dtxsid': ['DTXSID0020232'],
'chebi_id': ['CHEBI:27732', 'CHEBI:27134'],
'chembl': ['CHEMBL113'],
'nsc': ['NSC5036', 'NSC755917']}
The casrn list is PubChem's, and it contains placeholders such as
001-02-7 that are not CAS numbers.
Computed properties¶
CompoundProperties names every property PUG-REST can compute:
pc.get_compound_properties(2244, [CompoundProperties.XLOGP, CompoundProperties.TPSA,
CompoundProperties.COMPLEXITY,
CompoundProperties.HBOND_DONOR_COUNT],
include_synonyms=False)
{'CID': 2244,
'XLogP': 1.2,
'TPSA': 63.6,
'Complexity': 212,
'HBondDonorCount': 1,
'success': True,
'cid': 2244,
'error': None}
For many CIDs, get_compound_properties_batch asks in chunks of 200:
batch = pc.get_compound_properties_batch(
[2244, 2519, 3672, 1983],
[CompoundProperties.MOLECULAR_WEIGHT, CompoundProperties.XLOGP, CompoundProperties.TPSA])
pd.DataFrame(batch)[["cid", "MolecularWeight", "XLogP", "TPSA"]]
| cid | MolecularWeight | XLogP | TPSA | |
|---|---|---|---|---|
| 0 | 2244 | 180.16 | 1.2 | 63.6 |
| 1 | 2519 | 194.19 | -0.1 | 58.4 |
| 2 | 3672 | 206.28 | 3.5 | 37.3 |
| 3 | 1983 | 151.16 | 0.5 | 49.3 |
PUG-REST returns the molecular weight as text. The client passes the answer through unchanged.
Structures¶
pc.get_cids_by_smiles("CN1C=NC2=C1C(=O)N(C(=O)N2C)C"), pc.get_cids_by_inchikey("RYYVLZVUVIJVGH-UHFFFAOYSA-N")
([2519], [2519])
An InChI cannot travel in a URL path, so get_cids_by_inchi sends it in the
body of a POST:
pc.get_cids_by_inchi("InChI=1S/C8H10N4O2/c1-10-4-9-6-5(10)7(13)12(3)8(14)11(6)2/h4H,1-3H3")
[2519]
Substances¶
A substance is a record as a depositor submitted it (SID). Many SIDs map to one CID:
sids = pc.get_sids_by_name("8000-78-0")
substance = pc.get_substance_by_sid(sids[0])
len(sids), list(substance)
(27, ['sid', 'source', 'synonyms', 'xref', 'compound'])
Errors¶
PubChemAPI raises. PubChemNotFoundError means PubChem answered and has no
such record. PubChemError, its base class, covers every other failure,
including a request PubChem rejected:
from provesid import PubChemError, PubChemNotFoundError
for name, call in [("unknown name", lambda: pc.get_cids_by_name("xyzzy-no-such")),
("bad SMILES", lambda: pc.get_cids_by_smiles("not-a-smiles"))]:
try:
call()
except PubChemNotFoundError as e:
print(f"{name}: not found")
except PubChemError as e:
print(f"{name}: {type(e).__name__}: {str(e).splitlines()[0]}")
unknown name: not found
bad SMILES: PubChemError: HTTP 400 for https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/not-a-smiles/cids/JSON: {
A throttled request is retried with back-off. A host that asks for a long pause fails at once, without waiting. Answers, but not failures, are cached on disk. See Network behaviour and Caching.