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

Uploaded CPython 3.9+Windows x86-64

whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl (785.0 kB view details)

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

whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl (736.5 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

whiskeysour-0.1.1-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.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (561.3 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

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

Uploaded CPython 3.9+macOS 11.0+ ARM64

whiskeysour-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl (523.3 kB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: whiskeysour-0.1.1.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.1.tar.gz
Algorithm Hash digest
SHA256 b39d3cc489ce6297397aad4af307b143a48f9b8926d706970f4a997dd88e3049
MD5 849ade5c8b7c877e110a4d159e9f47c1
BLAKE2b-256 6aa38086f920d27df189b86096212113d35d96ced6305ca30c5a9d8656aeede7

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: whiskeysour-0.1.1-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.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 cc9197be2bc9a0bab9137e897c033e5019d84bda77010d84ec45fb6277b2f8aa
MD5 289b876ce1877ebde569dc9e5eb33943
BLAKE2b-256 f26425779cc8494e41d387b7728a2ecee87c44590ad4878f7cecc82c7e43dd13

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 4070efe3db17d25824a26e23aff13239cb6d6110b011ed9d31df0b90809c3ab0
MD5 9579c8e19d12d62088994eaa03e27f1c
BLAKE2b-256 caa1a9a11de935648957aed28f60fb893775efd818c3db376e97a0e84ac1c63a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 ae9dbb886ea66e9203025e8ce5c8b968fdd614c689dee77b3ad0537ef597656a
MD5 3e2747fcebe6450af9298aa70f153b92
BLAKE2b-256 fc82c20f993c59f0bd872d5e3226ed341c93b9ee350ade1f7aa2d6ef11a6a50b

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a324457941a348a4016e35eba35f6058e00e27004f3fbe63f786aaa070e3aa00
MD5 44471306ef04f4770522173ec9cadf4d
BLAKE2b-256 21f2acf4cd7f86c0a41a3f2ac30ae972b529a6d9d6fb117e4d7653fc22be843d

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e7255488a5fc6c1bcf5597317e00f2d33d9dcd9e19595760f1eb44caa2ab4c1d
MD5 241ba89472b6a4a2e3e99d80d9f50f75
BLAKE2b-256 79ee24d29f37402490191db5bc5d98ed0ed65ec5e92edea218bbf1b4a9921d40

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ec04b50e879f6faafd3cf6c6bf29d4188e74c25df6ff136a0bc8cb1e34da4ed3
MD5 f2f10519279311fde869ef71652204ec
BLAKE2b-256 abed16c86fc7a096ab811e483406329f2e27a0c982abbf259543c555370f8af5

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for whiskeysour-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 3404b15d694f879b21cbc5c4d40137019b2d1a6c7520fc38e2f3c0259ac25cb8
MD5 75ca648e5fa7714aa2dc5b33c93f935d
BLAKE2b-256 2bfcaf88a6d8e71ac7559d562832387afca7576620e4d8275bbd4838c59a971b

See more details on using hashes here.

Provenance

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