pygsrs is a Python client library for the FDA Global Substance Registration System (GSRS) REST API. It provides a simple, Pythonic interface for querying the FDA's authoritative database of regulated substances — chemicals, biologics, proteins, nucleic acids, and more.
All public functions return pandas.DataFrame objects (or a dict of them for gsrs_all). On error, they return None and emit a UserWarning instead of raising exceptions, making them safe to use in data pipelines.
The Global Substance Registration System (GSRS) is the FDA's authoritative database for identifying and registering substances. Every registered substance is assigned a UNII (Unique Ingredient Identifier) — a 10-character alphanumeric code (e.g. R16CO5Y76E for aspirin) that uniquely identifies a substance across regulatory systems worldwide.
With uv (recommended):
uv add pygsrsWith pixi:
pixi add --pypi pygsrsPython 3.10+ is required.
| Extra | Enables |
|---|---|
cache |
Disk-based HTTP caching via diskcache |
# uv
uv add "pygsrs[cache]"
# pixi
pixi add --pypi "pygsrs[cache]"import pygsrs
# Free-text search
results = pygsrs.gsrs_search("aspirin")
print(results[["approval_id", "preferred_name", "substance_class"]])
# Look up a substance by UNII
aspirin = pygsrs.gsrs_substance("R16CO5Y76E")
# Get all names (INN, USAN, trade names, synonyms …)
names = pygsrs.gsrs_names("R16CO5Y76E")
# Get external codes (CAS, NCI, WHO, …)
codes = pygsrs.gsrs_codes("R16CO5Y76E")
# Get chemical structure — by UNII
structure = pygsrs.gsrs_structure("R16CO5Y76E")
print(structure[["smiles", "formula", "mwt", "inchi_key"]])
# Get chemical structure — from ANY identifier (auto-detected)
pygsrs.gsrs_structure_from_id("aspirin") # name
pygsrs.gsrs_structure_from_id("50-78-2") # CAS
pygsrs.gsrs_structure_from_id("BSYNRYMUTXBXSQ-UHFFFAOYSA-N") # InChIKey
pygsrs.gsrs_structure_from_id("CC(=O)Oc1ccccc1C(=O)O") # SMILES
pygsrs.gsrs_structure_from_id("R16CO5Y76E") # UNII
# Search by SMILES (substructure, similarity, exact, flex)
hits = pygsrs.gsrs_structure_search("CC(=O)Oc1ccccc1C(=O)O", search_type="exact")
# All data for a UNII in one call
data = pygsrs.gsrs_all("R16CO5Y76E")
print(data["names"])
print(data["structure"])| Function | Description |
|---|---|
gsrs_search |
Free-text / Lucene search |
gsrs_search_all |
Auto-paginated free-text search |
gsrs_substance |
Look up a substance by UNII |
gsrs_batch |
Concurrent lookup for multiple UNIIs |
gsrs_names |
All registered names for a substance |
gsrs_codes |
All external codes (CAS, NCI, WHO, …) |
gsrs_structure |
Chemical structure by UNII |
gsrs_structure_from_id |
Chemical structure from any identifier |
gsrs_structure_search |
Structure search by SMILES |
gsrs_hierarchy |
Parent/child relationship hierarchy |
gsrs_browse |
Paginated browse of all substances |
gsrs_browse_all |
Auto-paginated browse of all substances |
gsrs_vocabularies |
Controlled vocabulary terms |
gsrs_unii_from_name |
Look up UNII by name |
gsrs_all |
All data for a substance in one call |
Full-text search using Lucene query syntax.
# Simple keyword search
df = gsrs_search("ibuprofen")
# Lucene field search
df = gsrs_search("_name:acetaminophen AND substanceClass:chemical")
# Paginated results
page1 = gsrs_search("antibiotic", top=50, skip=0)
page2 = gsrs_search("antibiotic", top=50, skip=50)Returns: DataFrame — columns: uuid, approval_id, preferred_name, substance_class, status, definition_type, definition_level, version, names_url, codes_url, self_url, date_retrieved.
Auto-paginate through all search results for a query and return them in a single DataFrame.
all_hits = gsrs_search_all("antibiotic")
print(f"Total results: {len(all_hits)}")Returns: DataFrame — same columns as gsrs_search.
Look up a single substance by its UNII code.
df = gsrs_substance("R16CO5Y76E") # Aspirin
print(df["preferred_name"].iloc[0]) # → 'ASPIRIN'Returns: Single-row DataFrame — same columns as gsrs_search plus query.
Fetch substance data for multiple UNIIs concurrently using a thread pool.
uniis = ["R16CO5Y76E", "6M3C89ZY6R", "4G7DS2Q64Y"] # aspirin, nicotine, caffeine
results = gsrs_batch(uniis)
print(results[["approval_id", "preferred_name"]])Returns: DataFrame combining results from all UNIIs with a query column. Failed lookups are skipped with a warning.
Retrieve all registered names for a substance (INN, USAN, trade names, synonyms, etc.).
names = gsrs_names("R16CO5Y76E")
print(names[["name", "type", "language", "preferred"]])Returns: DataFrame — columns: name, type, language, preferred, display_name, query, date_retrieved.
Retrieve all external identifier codes for a substance (CAS RN, NCI thesaurus code, WHO INN number, etc.).
codes = gsrs_codes("R16CO5Y76E")
print(codes[["code_system", "code", "type"]])
# Find the CAS number
cas = codes.loc[codes["code_system"] == "CAS", "code"].iloc[0] # → '50-78-2'Returns: DataFrame — columns: code_system, code, type, url, query, date_retrieved.
Retrieve the chemical structure record for a substance by UNII.
struct = gsrs_structure("R16CO5Y76E")
print(struct[["smiles", "formula", "mwt", "inchi_key", "stereochemistry"]])Returns: Single-row DataFrame — columns: smiles, formula, mwt, inchi_key, inchi, stereochemistry, optical_activity, stereo_centers, defined_stereo, ez_centers, charge, molfile, query, date_retrieved.
Returns an empty DataFrame for non-chemical substances (biologics, proteins, nucleic acids, etc.).
Retrieve the chemical structure for a substance from any common identifier. The function resolves the identifier to a UNII first, then calls gsrs_structure.
id_type |
Identifier shape | Resolution |
|---|---|---|
"unii" |
10-char alphanumeric — "R16CO5Y76E" |
Direct structure lookup |
"name" |
Any name / INN / synonym — "aspirin" |
gsrs_unii_from_name |
"smiles" |
SMILES string — "CC(=O)Oc1ccccc1C(=O)O" |
gsrs_structure_search (exact) |
"inchikey" |
27-char InChIKey — "BSYNRYMUTXBXSQ-UHFFFAOYSA-N" |
gsrs_structure_search (exact) |
"cas" |
CAS Registry Number — "50-78-2" |
gsrs_search (free-text) |
"auto" |
(default) | Inferred from string pattern |
Auto-detection rules (applied in order):
- Matches
^[A-Z0-9]{10}$→ UNII - Matches
^[A-Z]{14}-[A-Z]{10}-[A-Z]$→ InChIKey - Matches
^\d{2,7}-\d{2}-\d$→ CAS - Contains
=,#,@,[,(, etc. → SMILES - Anything else → name
# Auto-detect (recommended)
gsrs_structure_from_id("R16CO5Y76E") # → UNII
gsrs_structure_from_id("aspirin") # → name
gsrs_structure_from_id("CC(=O)Oc1ccccc1C(=O)O") # → SMILES
gsrs_structure_from_id("BSYNRYMUTXBXSQ-UHFFFAOYSA-N") # → InChIKey
gsrs_structure_from_id("50-78-2") # → CAS
# Override when needed
gsrs_structure_from_id("50-78-2", id_type="cas")
gsrs_structure_from_id("aspirin", id_type="name")Returns: Same single-row DataFrame as gsrs_structure, or None if the identifier cannot be resolved or the substance has no defined structure.
Search GSRS by chemical structure using a SMILES string.
search_type |
Description |
|---|---|
"sub" |
Substructure — find substances containing the query |
"sim" |
Similarity — Tanimoto ≥ cutoff |
"exact" |
Exact structure match |
"flex" |
Flexible / disconnected fragment search |
# Exact match
exact = gsrs_structure_search("CC(=O)Oc1ccccc1C(=O)O", search_type="exact")
# Similarity search (Tanimoto ≥ 0.9)
similar = gsrs_structure_search("CC(=O)Oc1ccccc1C(=O)O", search_type="sim", cutoff=0.9, top=20)
# Substructure search
subs = gsrs_structure_search("c1ccccc1", search_type="sub", top=50)Returns: DataFrame — same columns as gsrs_search plus query_smiles.
Retrieve the parent/child relationship hierarchy for a substance.
tree = gsrs_hierarchy("R16CO5Y76E")
print(tree[["text", "type", "depth", "approval_id"]])Returns: DataFrame — columns: text, type, depth, expandable, node_id, parent, approval_id, name, refuuid, substance_class, deprecated, query, date_retrieved.
Paginated browse of all substances in GSRS (no query filter).
page = gsrs_browse(top=100, skip=0)Returns: DataFrame — same columns as gsrs_search.
Auto-paginate through all substances in GSRS and return them in a single DataFrame. May take several minutes depending on network conditions.
all_substances = gsrs_browse_all()
print(f"Total substances: {len(all_substances)}")Returns: DataFrame — same columns as gsrs_search.
Retrieve all controlled vocabulary domains and their terms. Useful for understanding valid values for fields like substanceClass, name.type, code.codeSystem, etc.
vocab = gsrs_vocabularies()
print(vocab["domain"].unique())
# Filter to a specific domain
substance_classes = vocab.loc[vocab["domain"] == "SUBSTANCE_CLASS", "value"]Returns: DataFrame — columns: domain, term_type, value, display, hidden, selected, date_retrieved.
Look up UNII codes by substance name using an exact phrase match on root_names_name.
results = gsrs_unii_from_name("aspirin")
print(results[["approval_id", "preferred_name"]])Returns: DataFrame — same columns as gsrs_search plus query_name.
Fetch all available data for a substance in one call. Internally calls gsrs_substance, gsrs_names, gsrs_codes, gsrs_structure, and gsrs_hierarchy concurrently.
data = gsrs_all("R16CO5Y76E")
# data is a dict; each value is a DataFrame (or None for unavailable data)
print(data["substance"])
print(data["names"])
print(data["codes"])
print(data["structure"])
print(data["hierarchy"])Returns: dict[str, DataFrame | None]. Individual keys may be None (e.g. no structure for a biologic).
All public functions have async equivalents prefixed with a:
import asyncio
import pygsrs
async def main():
# Single async lookup
df = await pygsrs.agsrs_substance("R16CO5Y76E")
# Concurrent lookups
uniis = ["R16CO5Y76E", "6M3C89ZY6R", "4G7DS2Q64Y"]
results = await asyncio.gather(*[pygsrs.agsrs_substance(u) for u in uniis])
asyncio.run(main())Available async functions:
| Async | Sync equivalent |
|---|---|
agsrs_search |
gsrs_search |
agsrs_substance |
gsrs_substance |
agsrs_names |
gsrs_names |
agsrs_codes |
gsrs_codes |
agsrs_structure |
gsrs_structure |
agsrs_structure_search |
gsrs_structure_search |
agsrs_hierarchy |
gsrs_hierarchy |
agsrs_browse |
gsrs_browse |
agsrs_vocabularies |
gsrs_vocabularies |
agsrs_unii_from_name |
gsrs_unii_from_name |
agsrs_all |
gsrs_all |
pygsrs ships with a command-line interface for quick lookups from the terminal:
# Free-text search
pygsrs search aspirin
pygsrs search aspirin --top 20 --format csv
# Look up by UNII
pygsrs substance R16CO5Y76E
# Names, codes, hierarchy
pygsrs names R16CO5Y76E
pygsrs codes R16CO5Y76E
pygsrs hierarchy R16CO5Y76E
# Structure by UNII
pygsrs structure R16CO5Y76E
# Structure from any identifier (auto-detected)
pygsrs structure-from-id aspirin
pygsrs structure-from-id "50-78-2"
pygsrs structure-from-id "BSYNRYMUTXBXSQ-UHFFFAOYSA-N"
pygsrs structure-from-id "CC(=O)Oc1ccccc1C(=O)O"
pygsrs structure-from-id "50-78-2" --id-type cas # explicit type
# Structure search by SMILES
pygsrs structure-search "CC(=O)Oc1ccccc1C(=O)O" --type exact
pygsrs structure-search "c1ccccc1" --type sub --top 50
# Browse all substances (paginated)
pygsrs browse --top 20 --skip 0
# Controlled vocabularies
pygsrs vocabularies
# Look up UNII by name
pygsrs unii-from-name aspirin
# All data for a UNII
pygsrs all R16CO5Y76E
# Output formats: table (default), csv, json
pygsrs names R16CO5Y76E --format json
pygsrs search ibuprofen --format csvEnable disk-based HTTP caching to avoid redundant API calls (requires the cache extra — see Installation):
import pygsrs
# Enable caching (default location: ~/.cache/pygsrs)
pygsrs.enable_cache()
# Custom directory and TTL (seconds)
pygsrs.enable_cache(directory="/tmp/mygsrs", ttl=3600)
# Disable caching
pygsrs.disable_cache()
# Clear the cache
pygsrs.clear_cache()All functions use graceful error handling — on failure they return None and emit a UserWarning rather than raising an exception:
import warnings
import pygsrs
# Check for None
result = pygsrs.gsrs_substance("NOT-A-REAL-UNII")
if result is None:
print("Lookup failed")
# Capture the warning message
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = pygsrs.gsrs_substance("NOT-A-REAL-UNII")
if result is None:
print("Error:", w[-1].message)import pygsrs
# Point at a private GSRS deployment
pygsrs.set_base_url("https://my-gsrs-instance.example.com/api/v1")
# Reset to the public FDA instance
pygsrs.set_base_url()# Clone and install in editable mode with dev deps
git clone https://github.com/c1au6i0/pygsrs.git
cd pygsrs
# uv
uv sync --extra dev
# pixi
pixi install
# Run tests (fully offline — uses mocked HTTP via pytest-httpx)
pytest
# Linter
ruff check src/ tests/
# Type checker
mypy src/MIT