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

# Drop-in replacement for BeautifulSoup — no parser argument needed
soup = WhiskeySour(html)

# 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.4.tar.gz (48.2 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.4-cp39-abi3-win_amd64.whl (480.3 kB view details)

Uploaded CPython 3.9+Windows x86-64

whiskeysour-0.1.4-cp39-abi3-musllinux_1_2_x86_64.whl (809.1 kB view details)

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

whiskeysour-0.1.4-cp39-abi3-musllinux_1_2_aarch64.whl (759.1 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

whiskeysour-0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (573.5 kB view details)

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

whiskeysour-0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (579.5 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

whiskeysour-0.1.4-cp39-abi3-macosx_11_0_arm64.whl (518.8 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

whiskeysour-0.1.4-cp39-abi3-macosx_10_12_x86_64.whl (533.9 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

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

File metadata

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

File hashes

Hashes for whiskeysour-0.1.4.tar.gz
Algorithm Hash digest
SHA256 b39cd62321b7ef8b24464aea96a4d212001628e8676664ad2d8d234162e25f6d
MD5 aab38493f5f00ffcfdee6450e33283e3
BLAKE2b-256 bbcdc8cf7b75793b43669fdcf9b444eae86aa11d4eb37015397a471a226424b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4.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.4-cp39-abi3-win_amd64.whl.

File metadata

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

File hashes

Hashes for whiskeysour-0.1.4-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 d838bbc332c8c87060141d34135b9e96075d2947ee44778f4baf40c07eb0e9f8
MD5 c34186ea44aec4697a23672138f98c72
BLAKE2b-256 4dc654e76ea2a5c3b17116765109fb5d20a82f5e920ed8df0309f9870f112032

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4-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.4-cp39-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.4-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 9e65e08fbdccfd6dc7a65f7979f9b5c44ad9250e2773802ff5fd697a7a3e7846
MD5 6a022117441cec534f02192b2ae6618d
BLAKE2b-256 680e086c6513e0357fe38928de6c097a17b74b7a1ad79f5fe360b15055f7c18b

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4-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.4-cp39-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.4-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 f6297acd3e11a2ce2f575443a05dde3d42d4227073a08e5dcd90634b1e4fd763
MD5 e04206e02736bac6f9ea4819eecf3646
BLAKE2b-256 cfd50194fb1b7a6e1b1bfd59cf9e39ccd9602dd3d88afc94a34d8c40028cc2c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4-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.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 cf454c9a93b4947e13f15a41cc85a0bfe9803a9f71d4443a1118bf4e6bdec17f
MD5 217497ae77a4465cf7f226ff997ea7c6
BLAKE2b-256 01c9c00b91e96daebd3470b9e5c79f864c9612505e7fa245f6baf07a0d25143d

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4-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.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 f5db79034792e380b302a1e368aa383d2a4235b20a7a9bb58619b554616a4db5
MD5 1713af7a8636daeb676b073b3f6db017
BLAKE2b-256 b6b7cf434bf42bb8426eb79564f80aa6231eee3521c7fad51d284491364d4573

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4-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.4-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.4-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7a7b6e77e9df0168d51aaa466792bf533383ded9f9ba5e1a37ae204595c68263
MD5 7fda878fcdf2c95459cdf14ebc81693d
BLAKE2b-256 b947ecb3b1846b9ccda43fd650f6a663fa6c79c2a66515978f42b6ce61bbcd83

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4-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.4-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for whiskeysour-0.1.4-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 dfcec346ac8d9045851d88f167341877eafd8c61ad9583a67a5f202700fa1118
MD5 e50a844a67c434c53c8f074ba8c61445
BLAKE2b-256 c38bf3afeda3feed1fbdbd3985370569d0484642b7036294e612a41e5b9b59f4

See more details on using hashes here.

Provenance

The following attestation bundles were made for whiskeysour-0.1.4-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