Skip to main content

High-performance BeautifulSoup replacement written in Rust

Project description

WhiskeySour

WhiskeySour logo

A high-performance drop-in replacement for Python's BeautifulSoup, written in Rust and published as a native Python package via PyO3.

Status: Beta — core implementation complete. 450 unit tests passing, 508 including integration tests.


Why WhiskeySour?

BeautifulSoup is beloved but slow. Every node is a Python object (~500 bytes), parsing is GIL-bound, and CSS selectors re-parse on every call. WhiskeySour fixes this at the foundation.

All numbers below are medians from a dev build (maturin develop). Release builds (maturin develop --release) are typically 2–3× faster still.

Operation WhiskeySour bs4 + html.parser Speedup
Parse 10KB 0.33 ms 3.78 ms 11×
Parse 100KB 4.08 ms 42.87 ms 11×
Parse 500KB 9.99 ms 106.37 ms 11×
find(id=…) 0.21 ms 2.21 ms 11×
find_all(class_=…) 0.62 ms 4.41 ms
select("div.item") 0.64 ms 8.92 ms 14×
get_text() 0.17 ms 0.68 ms
str() (serialize) 0.43 ms 21.58 ms 50×
tag.get("class") 0.29 µs 7.0 µs 24×
Memory per node ~40 bytes ~500 bytes 12× less

Key implementation choices:

  • Rust core via PyO3 + maturin
  • html5ever — spec-compliant HTML5 parser (same as Firefox/Chrome)
  • Arena allocation — compact ~40 byte/node layout vs ~500 bytes in bs4
  • cssparser — CSS selectors compiled to DFA, LRU-cached
  • GIL release — all Rust tree operations run outside the Python GIL
  • memchr — SIMD byte scanning (SSE2 / AVX2 / NEON)

API — drop-in compatible with BeautifulSoup

from whiskeysour import WhiskeySour

# Same as BeautifulSoup(html, "html.parser")
soup = WhiskeySour(html, "html.parser")

# All standard bs4 operations work identically:
soup.find("h1")
soup.find_all("a", class_="external")
soup.select("div.container > p:first-child")
soup.title.string
soup.find(id="main").get_text(strip=True)

# BS4-compatible NavigableString (name is None, exactly like bs4)
for child in tag.children:
    if child.name:          # None for text nodes, str for elements — same as bs4
        print(child.name)

# Drop-in alias
from whiskeysour import BeautifulSoup   # same class, different name

WhiskeySour extensions (not in bs4)

# Pre-compiled CSS selector — zero parse overhead on repeated use
q = soup.compile("div.item > a[href]")
for doc in documents:
    results = q.select(doc)

# Streaming parser — feed chunks incrementally
from whiskeysour import StreamParser, parse_stream

with StreamParser() as parser:
    for chunk in response.iter_content(4096):
        parser.feed(chunk)
soup = parser.close()

# Generator-style streaming with automatic extraction
import io
with open("large.html", "rb") as f:
    for article in parse_stream(f, selector="article.post"):
        print(article.find("h1").get_text())

BS4 compatibility notes

WhiskeySour is a faithful drop-in for the vast majority of BeautifulSoup code. A handful of behaviours differ due to html5ever's spec compliance:

Behaviour WhiskeySour BeautifulSoup
NavigableString.name None (identical to bs4) None
prettify(indent=N) Supported (alias for indent_width) Supported
</br> in source Creates 2 <br> (HTML5 spec) Creates 1 <br>
Duplicate attributes Keeps first (HTML5 spec) Keeps last
Null bytes \x00 Stripped (HTML5 spec) Passed through
Attribute order in str() Insertion order Alphabetical

The first two rows are identical; the remaining differences only affect malformed HTML.


Project Structure

WhiskeySour/
├── Cargo.toml                  # Rust workspace root
├── pyproject.toml              # maturin build config
├── pytest.ini                  # test configuration
│
├── crates/
│   ├── whiskeysour-core/        # Pure Rust library (no Python deps)
│   │   └── src/
│   │       ├── parser/         # html5ever integration
│   │       ├── node.rs         # Arena-allocated node pool
│   │       ├── selector/       # CSS selector DFA + LRU cache
│   │       ├── traversal/      # Tree iterators
│   │       ├── query/          # find() / find_all() / select()
│   │       └── serialize/      # HTML serialisation + prettify
│   │
│   └── whiskeysour-py/          # PyO3 bindings layer
│       └── src/
│           └── lib.rs          # _Tag, _Document Python classes
│
├── python/
│   └── whiskeysour/
│       ├── __init__.py         # Public API + BeautifulSoup alias
│       └── _core.pyi           # Type stubs for Rust extension
│
└── tests/
    └── python/
        ├── conftest.py
        ├── unit/               # 450 tests across 10 files
        ├── integration/        # bs4 API parity tests (58 tests)
        ├── performance/        # pytest-benchmark suites + comparison report
        └── fuzz/               # Hypothesis property tests (16 tests)

Quick start

# Prerequisites: Python 3.9+, Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate        # macOS / Linux

# Install dependencies
pip install maturin pytest pytest-benchmark hypothesis beautifulsoup4

# Build the Rust extension
maturin develop                  # dev build (fast to compile)
# maturin develop --release      # optimised (use for benchmarks)

# Run tests
pytest tests/python/unit/

Running tests

# All unit tests (fastest, no extra deps needed)
pytest tests/python/unit/ -q

# Single test file
pytest tests/python/unit/test_parsing.py -v

# Integration tests (requires beautifulsoup4)
pytest tests/python/integration/ -v

# Skip slow tests (large documents, deep nesting)
pytest -m "not slow"

# Fuzz / property-based tests (requires hypothesis)
pytest tests/python/fuzz/ -v

# Benchmark suites (requires pytest-benchmark)
pytest tests/python/performance/ --benchmark-only -v

# Performance comparison report (WhiskeySour vs BeautifulSoup)
python tests/python/performance/bench_comparison.py
python tests/python/performance/bench_comparison.py --fixture small --rounds 50
python tests/python/performance/bench_comparison.py --output /tmp/report.html
open bench_report.html

Test file overview

File Tests Covers
unit/test_parsing.py 67 HTML5 parsing, fragments, malformed HTML, void elements
unit/test_encoding.py 31 UTF-8/16, Latin-1, BOM, meta charset, surrogate pairs
unit/test_find.py 60 find/find_all by tag/id/class/attr/string/regex/lambda
unit/test_css_selectors.py 71 CSS3 + :has/:is/:where, structural pseudo-classes
unit/test_tree_navigation.py 67 parent/children/siblings/descendants/.string/.strings
unit/test_modification.py 49 decompose/extract/replace_with/insert/append/wrap
unit/test_output.py 43 str()/prettify()/encode()/round-trip stability
unit/test_edge_cases.py 44 10k+ nodes, deep nesting, concurrency, control chars
unit/test_streaming.py 19 StreamParser push API, parse_stream() generator
unit/test_css_selectors.py 71 CompiledSelector, cached selectors
integration/test_bs4_compat.py 58 Every public bs4 API, cross-library parity
fuzz/fuzz_parser.py 16 Hypothesis: no crash, valid UTF-8, round-trip stable

Test markers

Marker Description
slow Large document tests — skip with -m "not slow"
perf Benchmark tests — requires --benchmark-only

Development

# Rust checks (no build required)
~/.cargo/bin/cargo check -p whiskeysour-py

# Dev build (fast recompile, debug symbols)
maturin develop

# Release build (use for perf work)
maturin develop --release

# Rust tests
cargo test

# Formatting / linting
cargo fmt
cargo clippy

Building wheels

maturin build --release
maturin build --release --interpreter python3.9 python3.10 python3.11 python3.12 python3.13
maturin publish

Contributing

  1. All changes must be accompanied by tests
  2. Run pytest tests/python/unit/ -m "not slow" before submitting
  3. Run cargo fmt and cargo clippy for Rust changes
  4. Performance regressions > 5% against the baseline will block merge

Licence

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

whiskeysour-0.1.3.tar.gz (48.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

whiskeysour-0.1.3-cp39-abi3-win_amd64.whl (475.0 kB view details)

Uploaded CPython 3.9+Windows x86-64

whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_x86_64.whl (799.3 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ x86-64

whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_aarch64.whl (749.5 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (569.4 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (574.3 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

whiskeysour-0.1.3-cp39-abi3-macosx_11_0_arm64.whl (510.6 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

whiskeysour-0.1.3-cp39-abi3-macosx_10_12_x86_64.whl (521.6 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file whiskeysour-0.1.3.tar.gz.

File metadata

  • Download URL: whiskeysour-0.1.3.tar.gz
  • Upload date:
  • Size: 48.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for whiskeysour-0.1.3.tar.gz
Algorithm Hash digest
SHA256 c9262898bd0205fe9eb2f8e3d14b0cc1e32ed7a440cee242b56ea1ba762ecb1f
MD5 275ce766a9e25b91b1334959064f2c36
BLAKE2b-256 eaf32f30faf2b9f53099ed73adaeb26bed7320a1205a34ca54e54285fd704a95

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3.tar.gz:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.3-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: whiskeysour-0.1.3-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 475.0 kB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for whiskeysour-0.1.3-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 b4d83eb5f24c1ee60648a290f688ffa1d333ee512ac16d75c6409ddac7f9bcb7
MD5 c074029b79e69f288a44b22994992ce0
BLAKE2b-256 0b62f4190b8139b93d638c75b4e0cbbdef06fddcc0afd3d32d921e3bb8759d69

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3-cp39-abi3-win_amd64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 26bc786281b7a046492d441c3f946cbc4a584d83d5857805b08c37b366ba33b1
MD5 ca30ba2f692dafb25a235d8cfba36ace
BLAKE2b-256 effaef4c82f3fb01d11ee8167c7941662ae2cd66bf0f52e6c866009c16df19c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_x86_64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 e0ed889deaf2b0594f64d1fe90c2e6f7d800ad04be77adbe8f4a1205fcbd27ca
MD5 440aa09a12343edb109a16a04b94a674
BLAKE2b-256 dbd8c03c18d4cecd9219101f5443962e97e53a5d392f355cf3d19ea10294b963

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3-cp39-abi3-musllinux_1_2_aarch64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 b88faf5a6912b4b913ae5e2da3a47e57e2c90a0122e7250f4214d649cb9692db
MD5 47cb77d97d071ecb6daa48a849f938aa
BLAKE2b-256 414a704bd1abe0b38a7e1605bf26b2001296f60251a2df4ecdc6f4dc52b5e857

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 94f5038bc0d23473f066ea7a2f40615c14b6847dd6273800fa7c06a58127b225
MD5 8d3e37685058812f6691379b98e850bd
BLAKE2b-256 074aff92b3966fa96173122d172e2f72f0ed3ebd0b4b236f362df25b2bd9de8f

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.3-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.3-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 80957caaa8179128bf3ea57c75de6288265eec1308d0547b2687ddb57a66011d
MD5 e19aaeefc1a3544a988f2ad13789c0a2
BLAKE2b-256 cc10bcd46afac0a4ade65e7af50b3320f3f260f5164566d5f37c82fb70f32ea9

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whiskeysour-0.1.3-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.3-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 15a292248cb83cd3d779852262d67beaed61f0f894e1a81c0bc461eca2c88253
MD5 6f7c170ab867d5fee39191749d68fa3b
BLAKE2b-256 e41e01ebc75b31761d5e22ed99afb7080eeca1b5b4c47dec15ff53a4bab86a3a

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.3-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on the-pro/WhiskeySour

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page