Skip to content
ReShiki / guide

Python package

ReShiki’s experimental Python API draws, converts and analyzes structures with the editor’s Rust engine. It requires Python 3.10 or later and runs without the desktop application. Install it with uv add reshiki or pip install reshiki. From 0.14.0, each ReShiki release publishes abi3 wheels with the app’s version for macOS, Windows and Linux on x64 and ARM64; see Releasing.

import reshiki
aspirin = reshiki.Molecule.from_smiles("CC(=O)Oc1ccccc1C(=O)O")
styled = aspirin.clean().with_style("JACS / ACS", font_size=12)
styled.save("aspirin.png", width=300)
svg = styled.svg()
pdf = styled.pdf()
print(aspirin.properties().formula)

Molecule, Reaction and Drawing are immutable. clean() and with_style(style, **overrides) return new objects, so they can be chained. Jupyter displays each object as SVG.

properties() returns a frozen dataclass with the same fields as reshiki --cli analyze: formula, display_formula, mass, exact_mass, logp, tpsa, donors, acceptors, rings, inchi, inchikey, smiles and unpaired_electrons.

Use explicit constructors from_smiles(text), from_molfile(text), from_inchi(text), from_cdxml(text) and from_name(name, online=False). Names resolve offline by default. Pass online=True to look up a complete name in PubChem instead:

aspirin = reshiki.Molecule.from_name("aspirin", online=True)

Online lookup requires a unique complete-name match and imports stereospecific SMILES without dropping components. ReShiki verifies that layout preserves its molecular identity; ambiguous or unsupported results raise ValueError. Connection failures and request timeouts raise their corresponding Python exceptions. Each network request has a 15-second timeout. Offline lookup never contacts PubChem.

reshiki.parse(text, format=None) detects the input format. Drawing.open(path) uses a file’s extension, including extensionless POSCAR and CONTCAR names. It accepts native .rsk, MOL/SDF, RXN, SMILES/reaction SMILES, InChI, CDXML, binary CDX, CIF, PDB/ENT, POSCAR/CONTCAR/.vasp and XYZ/extXYZ. Crystal imports use the existing crystal readers.

Render with png(), svg() or pdf(), and export with cdxml(), mol(), sdf(), smiles(), inchi() or rsk(). PNG, PDF and native drawings return bytes; chemical text and SVG return strings. save(path, **options) chooses the output format from the extension. EMF raises NotImplementedError pending a headless exporter.

drawing = reshiki.Drawing.open("rutile.vasp")
drawing.save("rutile.svg")
png = reshiki.render("CCO", format="png", width=300)
mol = reshiki.convert("CCO", to="mol")
analysis = reshiki.properties("CCO")

reshiki.styles() lists the editor’s document styles. Create a reusable reshiki.Style("JACS / ACS", font_size=12), pass it as style= to a render method, or supply overrides directly, such as molecule.svg(font_size=12). Overrides use points for physical dimensions.

Bind a style once to reuse it across constructors and functional exports:

custom_style = reshiki.with_style("JACS / ACS", font_family="sans-serif", font_size=12)
aspirin = custom_style.Molecule.from_smiles("CC(=O)Oc1ccccc1C(=O)O")
named = custom_style.Molecule.from_name("aspirin", online=True)
svg = custom_style.render("CCO", "svg", width=300)
pngs = custom_style.render_many(["CCO", "c1ccccc1O"], width=300)

The context offers Drawing, Molecule and Reaction constructors and the same parse, render, convert and render_many operations as the module. Returned objects keep their normal types. The equivalent object-first form is reshiki.Molecule.from_smiles(text).with_style(custom_style.style).

Contexts are immutable. custom_style.with_style(font_size=14) combines new overrides with the saved settings; the latest value for a setting wins. Passing an explicit style name or Style selects a new base. Per-call export overrides take precedence over the context’s settings. Configuration can be applied before or after construction; clean() remains an operation on the drawing. Saved styles include explicit default values, so applying a reset context also turns off settings already enabled on a drawing. Keyword-only updates to an individual drawing preserve its other settings.

Labels use the requested font when available, then generic sans-serif. Native builds include a final sans-serif fallback, so installed Liberation Sans is not required. After installing system fonts, restart Python or the Jupyter kernel before rendering again.

The same aspirin drawing exported at 600 dpi illustrates the fallback behavior. The before images use PyPI 0.14.0; the after images use the corrected renderer. Each pair shares the same physical scale and white framing.

Font environment Before After
No discoverable fonts Aspirin loses atom labels without system fonts Bundled sans fallback retains all aspirin labels
Arial unavailable Aspirin uses a CJK fallback before the correction Aspirin uses the generic sans-serif fallback

PNG size follows the style’s physical bond length and its default resolution. Use dpi= to set resolution, or width= and/or height= to fit a pixel box while preserving the aspect ratio.

render_many(items, format="png", errors="none", **options) renders in parallel while releasing Python’s GIL. It returns a list with None for failed rows and an .errors list of (index, message) pairs. Use errors="raise" to raise on a failed row.

pngs = reshiki.render_many(["CCO", "C1CC"], width=300)
print(pngs.errors)

The Excel extra uses openpyxl and embeds PNG images:

import reshiki
reshiki.excel.add_structures(
"compounds.xlsx", smiles="A", into="B",
out="compounds-drawn.xlsx", width=300, header=True,
)

Install reshiki[excel] for this helper. The header defaults to row 1; use header=False to render from row 1, or start_row= to choose the first row.

Install reshiki[pandas] and explicitly import reshiki.pandas to register the pandas Series accessor:

import pandas as pd
import reshiki.pandas
series = pd.Series(["CCO", "O"])
images = series.reshiki.png(width=300)
descriptors = series.reshiki.properties()

The accessor provides .png(), .svg() and .properties(). Optional dependencies load when their integration is used. RDKit interoperability uses Molecule.from_rdkit(mol) and molecule.to_rdkit() through MOL blocks when RDKit is installed.

Editable Office pictures depend on issue #462. EMF export remains a follow-up.

From the repository root, with Rust and uv installed:

Terminal window
uv run --no-project --python 3.12.15 python scripts/license_notices.py
uvx [email protected] build --manifest-path crates/python/Cargo.toml --locked --out artifacts/python
uv run --no-project --python 3.12.15 python scripts/check_python_wheel_notices.py 'artifacts/python/*.whl'
uv sync --project crates/python --all-extras --no-install-project
uv run --project crates/python --no-sync --with ./artifacts/python/*.whl --with rdkit pytest crates/python/tests
uv run --project crates/python --no-sync ruff check crates/python/python crates/python/tests
uv run --project crates/python --no-sync ruff format --check crates/python/python crates/python/tests
uv run --project crates/python --no-sync ty check --project crates/python

The package version comes from the Rust workspace version. No desktop executable or external InChI helper is required by the wheel. The ignored crates/python/wheel-licenses/ staging directory contains the project licenses and consolidated notices for the selected target’s Rust dependencies. Pass --target to the notice generator when cross-compiling. Maturin includes these files under the wheel’s .dist-info/licenses/ and declares them with License-File metadata.