PubMed Client - Python Bindings

PyPI version Python 3.12+ License: MIT

Python bindings for the PubMed and PMC (PubMed Central) API client library.

Overview

This package provides Python bindings to the Rust-based PubMed client library, enabling high-performance access to PubMed and PMC APIs from Python.

Features

  • PubMed API: Search and retrieve article metadata

  • PMC API: Access full-text articles from PubMed Central

  • ELink API: Get related articles, citations, and PMC links

  • Europe PMC API: Cross-source search (preprints, patents, Agricola, CBA), JATS full text, reference and citation graphs, external database links — no API key needed

  • SearchQuery Builder: Build complex queries programmatically with filters

  • High Performance: Built with Rust for speed and reliability

  • Type-Safe: Full type hints for better IDE support

Installation

From PyPI

pip install pubmed-client-py

With uv

uv add pubmed-client-py

From Source

# Clone the repository
git clone https://github.com/illumination-k/pubmed-client.git
cd pubmed-client/pubmed-client-py

# Create virtual environment and install
uv venv
uv run --with maturin maturin develop

Quick Start

Basic Usage

import pubmed_client

# Create a unified client
client = pubmed_client.Client()

# Search PubMed
articles = client.pubmed.search_and_fetch("covid-19 vaccine", max_results=10)

for article in articles:
    print(f"Title: {article.title}")
    print(f"PMID: {article.pmid}")
    print(f"Journal: {article.journal}")
    print()

Fetch Single Article

import pubmed_client

client = pubmed_client.Client()

# Fetch article by PMID
article = client.pubmed.fetch_article("31978945")

print(f"Title: {article.title}")
print(f"Authors: {article.author_count}")
print(f"Abstract: {article.abstract_text[:200]}...")

# Access authors and affiliations
for author in article.authors():
    print(f"  {author.full_name}")
    if author.orcid:
        print(f"    ORCID: {author.orcid}")

Fetch PMC Full-Text

import pubmed_client

client = pubmed_client.Client()

# Fetch full text from PMC
full_text = client.pmc.fetch_full_text("PMC7906746")

print(f"Title: {full_text.title}")
print(f"Sections: {len(full_text.sections())}")
print(f"References: {len(full_text.references())}")

# Access article sections
for section in full_text.sections():
    print(f"Section: {section.title}")
    print(f"Content: {section.content[:200]}...")

# Access figures and tables
for figure in full_text.figures():
    print(f"Figure: {figure.label}")
    print(f"Caption: {figure.caption}")

# Convert to Markdown
markdown = full_text.to_markdown()
print(markdown)

Citation Analysis

import pubmed_client

client = pubmed_client.Client()

# Get citations for an article
citations = client.get_citations([31978945])
print(f"Citation count: {len(citations)}")

# Get citing article PMIDs
for citing_pmid in citations.citing_pmids[:10]:
    print(f"Cited by: {citing_pmid}")

Using SearchQuery Builder

import pubmed_client

client = pubmed_client.Client()

# Build a complex query
query = (
    pubmed_client.SearchQuery()
    .query("cancer")
    .published_between(2020, 2024)
    .article_type("Clinical Trial")
    .free_full_text_only()
    .limit(50)
)

# Execute the search
articles = client.pubmed.search_and_fetch(query, 0)  # limit ignored when using SearchQuery

for article in articles:
    print(f"[{article.pmid}] {article.title}")

Field, MeSH, and Validation Filters

import pubmed_client

query = (
    pubmed_client.SearchQuery()
    .title_or_abstract("gene therapy")
    .author("Doudna JA")
    .journal("Nature")
    .mesh_term("Neoplasms")
    .mesh_major_topic("Genetic Therapy")
    .affiliation("Harvard")
    .language("english")
    .human_studies_only()
    .has_abstract()
)

# Raises InvalidQueryException if the query is malformed
query.validate()

# Drop duplicate terms/filters, then inspect the query
query.optimize()
terms, filters, complexity = query.get_stats()

print(query.build())

Also available: first_author(), last_author(), journal_abbreviation(), title_contains(), abstract_contains(), grant_number(), isbn(), issn(), mesh_terms(), mesh_subheading(), orcid(), organism_mesh(), animal_studies_only(), age_group(), and custom_filter() for raw PubMed syntax.

Boolean Query Combinations

import pubmed_client

# Build complex queries with boolean logic
q1 = pubmed_client.SearchQuery().query("covid-19")
q2 = pubmed_client.SearchQuery().query("vaccine")
q3 = pubmed_client.SearchQuery().query("efficacy")

# Combine with AND
combined = q1.and_(q2).and_(q3)
print(combined.build())  # ((covid-19) AND (vaccine)) AND (efficacy)

# Combine with OR
either = q1.or_(q2)
print(either.build())  # (covid-19) OR (vaccine)

# Exclude specific terms
base = pubmed_client.SearchQuery().query("treatment")
excluded = pubmed_client.SearchQuery().query("animal studies")
human_only = base.exclude(excluded)
print(human_only.build())  # (treatment) NOT (animal studies)

Extract Figures from PMC Articles

import pubmed_client

client = pubmed_client.Client()

# Download and extract figures with captions
figures = client.pmc.extract_figures_with_captions("PMC7906746", "./output")

for fig in figures:
    print(f"Figure: {fig.figure.label}")
    print(f"Caption: {fig.figure.caption}")
    print(f"File: {fig.extracted_file_path}")
    print(f"Size: {fig.file_size} bytes")
    if fig.dimensions:
        print(f"Dimensions: {fig.dimensions[0]}x{fig.dimensions[1]}")

Europe PMC

Europe PMC complements the NCBI E-utilities: it indexes preprints (PPR), patents (PAT), Agricola (AGR) and Chinese Biological Abstracts (CBA) alongside PubMed (MED) and PMC, and needs no API key.

Records are addressed by a source database plus an id. Every method accepts the id bare ("PMC3258128", "33515491"), with an explicit source, or fully qualified ("PPR/PPR123456"). Given no source, a PMC-prefixed id is read as a PMC record and anything else as a PubMed record.

import pubmed_client

client = pubmed_client.Client()

# Cross-source search, including preprints
for result in client.europe_pmc.search("TITLE:CRISPR AND SRC:PPR", 5):
    print(result.europe_pmc_id, result.title)

# Full text as a parsed article, or as raw JATS XML for non-PMC sources
article = client.europe_pmc.fetch_full_text("PMC3258128")
xml = client.europe_pmc.fetch_full_text_xml("PMC3258128")

# Citation graph in both directions
references = client.europe_pmc.get_references("PMC3258128")
citations = client.europe_pmc.get_citations("33515491", source="MED")

# Cross-references to external databases (UniProt, EMBL, PDB, ...)
for link in client.europe_pmc.get_database_links("PMC3258128"):
    print(link.db_name, link.db_count)

# `resultType="core"` returns far more than is modelled; the remainder is
# available as a plain dict rather than pinned to a schema Europe PMC may change
for result in client.europe_pmc.search_all("malaria vaccine", 10, result_type="core"):
    print(result.extra().get("citedByCount"))

Paging is explicit where Europe PMC exposes it — search_page returns a next_cursor_mark, and get_references_page / get_citations_page return (hit_count, entries).

Configuration

With API Key

Using an NCBI API key increases rate limits from 3 to 10 requests per second:

import pubmed_client

config = (
    pubmed_client.ClientConfig()
    .with_api_key("your_ncbi_api_key")
    .with_email("your@email.com")
    .with_tool("YourAppName")
)

client = pubmed_client.Client.with_config(config)

Rate Limiting

import pubmed_client

config = (
    pubmed_client.ClientConfig()
    .with_rate_limit(2.0)  # 2 requests per second
    .with_timeout_seconds(60)  # 60 second timeout
)

client = pubmed_client.Client.with_config(config)

Type Hints and IDE Support

The package includes complete type stubs (.pyi files) for full IDE autocomplete and type checking:

import pubmed_client

# Type hints work automatically
config: pubmed_client.ClientConfig = pubmed_client.ClientConfig()
client: pubmed_client.Client = pubmed_client.Client.with_config(config)
articles: list[pubmed_client.PubMedArticle] = client.pubmed.search_and_fetch("query", 10)

Run type checking with mypy:

mypy your_script.py

Maintaining the type stub

pubmed_client.pyi is generated, not hand-edited. It is produced from the #[gen_stub_pyclass] / #[gen_stub_pymethods] annotations in the Rust source by the stub_gen binary, which then splices in the members pyo3-stub-gen cannot introspect (__version__ and the create_exception! hierarchy).

When you add or change a PyO3 class, method, or attribute:

  1. Annotate the new #[pyclass] with #[gen_stub_pyclass] and every #[pymethods] block with #[gen_stub_pymethods] (module-level items added via m.add(...), such as new exceptions, also need to be listed in stub_gen.rs).

  2. Regenerate the stub:

    MISE_ENV=python mise run stubgen:py     # or: cargo run --bin stub_gen
    
  3. Verify it matches the compiled module:

    MISE_ENV=python mise run stubtest:py    # maturin develop + python -m mypy.stubtest
    
  4. Commit the regenerated pubmed_client.pyi.

CI enforces both halves: the Python Type Stub Check job regenerates the stub and fails on git diff if the checked-in copy is stale, then runs stubtest so the stub can never silently drift from the compiled module. stubtest-allowlist.txt records the intentionally-unstubbed internal extension submodule.

Development

Prerequisites

  • Python >= 3.12

  • Rust toolchain (installed via rustup)

  • uv (for Python package management)

  • maturin (for building Python bindings)

Setup Development Environment

# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create virtual environment
cd pubmed-client-py
uv venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Build and install in development mode
uv run --with maturin maturin develop

Running Tests

# Install dev dependencies
uv sync --group dev

# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=pubmed_client

Code Quality

# Format code
uv run ruff format

# Lint code
uv run ruff check

# Type checking
uv run mypy tests/

Building

# Build wheel
uv run --with maturin maturin build --release

# Build for distribution
uv run --with maturin maturin build --release --sdist

Publishing

# Publish to PyPI (requires credentials)
uv run --with maturin maturin publish

License

MIT