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.2.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.2-cp39-abi3-win_amd64.whl (462.9 kB view details)

Uploaded CPython 3.9+Windows x86-64

whiskeysour-0.1.2-cp39-abi3-musllinux_1_2_x86_64.whl (784.9 kB view details)

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

whiskeysour-0.1.2-cp39-abi3-musllinux_1_2_aarch64.whl (736.6 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

whiskeysour-0.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (555.6 kB view details)

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

whiskeysour-0.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (561.5 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

whiskeysour-0.1.2-cp39-abi3-macosx_11_0_arm64.whl (508.6 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

whiskeysour-0.1.2-cp39-abi3-macosx_10_12_x86_64.whl (523.4 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: whiskeysour-0.1.2.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.2.tar.gz
Algorithm Hash digest
SHA256 d43f21cb34db595ea7fdd495651117d30b47dac1f6ae6760521bb9a79f069f77
MD5 e5907f043e8961d06ae2df35e9dc7413
BLAKE2b-256 75cf2defc3447808ff6ca292fcb89e056d4b15bf0685ddd17fd2f919720573a0

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: whiskeysour-0.1.2-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 462.9 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.2-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 29444c6385e261a7a05ac1bf80fa220af5dcc6c99429d344326c43cbc9fca6ef
MD5 3843c1d5b2c4496e80adff10f2fe1473
BLAKE2b-256 1e23c7c3fb65faf3a0aae208f67a36460f5df3bce35607da86ff862c5e91ee39

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.2-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 91423e277eb10d1bdd402082f59e98d45e2985296e113a13b62d9b537f0fbd9a
MD5 368b2656ff337b0d44172901b74aa1ab
BLAKE2b-256 3cdedf80d05c777222de33ef0c599dee2d74745022c79a3b8a1e10bb6c436fe0

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.2-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 c9f77fe5d886bfd2839224c6c8f88b80e3a030adf5f1f91fbbd7df0559f3f693
MD5 301b37248c6f0dd6cf8a0006e6518216
BLAKE2b-256 7274f02c9dafaa8b498431b6bb69c3ac44c0c6cd409309e993e3aca3eedeb506

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1bee1b910c7fd340b64bb341f87d2c2bf27a58fe948f69b1ff24877885faa09f
MD5 e80d7be0ca8ef06af61d6b19c95f0fec
BLAKE2b-256 9f3826f78b63cf5035d0fae2d02f46190caa9f80fbbdcd3357dd1b77e808c2dc

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 50990e6ff4427b7a4ef6607b4f228693a0521b40ef0389e3473558f89e49af4d
MD5 8180c6b202ed34b55e4fbb8a6e5c60cb
BLAKE2b-256 8466d05771dc92bd4c02427d77b4b13b77c37c2a0dd0f70badc7cee2a3ae2930

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.2-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e102c0aedeaf7c2a882d61a6ee984a85792ef0f8d10d4d9e529005c3c603d239
MD5 c622c1bc629e5e3b29b9449bba941c70
BLAKE2b-256 cc212700847b54eeb38f3d3b80c8cc796e5fc44ebbf687521ea731052e6d809a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.2-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 8d790e86915cf816dac62c8f6ed2f699c68c237b1324180cd00c77eb81a0fc3c
MD5 1d13e22234c14a7de58b89ba540c86db
BLAKE2b-256 78d894d8d1ea4785fbd58966843223350fa8a763879c78ddd7707712604d8138

See more details on using hashes here.

Provenance

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