High-performance BeautifulSoup replacement written in Rust
Project description
WhiskeySour
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 | 7× |
select("div.item") |
0.64 ms | 8.92 ms | 14× |
get_text() |
0.17 ms | 0.68 ms | 4× |
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
- All changes must be accompanied by tests
- Run
pytest tests/python/unit/ -m "not slow"before submitting - Run
cargo fmtandcargo clippyfor Rust changes - 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
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b39d3cc489ce6297397aad4af307b143a48f9b8926d706970f4a997dd88e3049
|
|
| MD5 |
849ade5c8b7c877e110a4d159e9f47c1
|
|
| BLAKE2b-256 |
6aa38086f920d27df189b86096212113d35d96ced6305ca30c5a9d8656aeede7
|
Provenance
The following attestation bundles were made for whiskeysour-0.1.1.tar.gz:
Publisher:
release.yml on the-pro/WhiskeySour
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1.tar.gz -
Subject digest:
b39d3cc489ce6297397aad4af307b143a48f9b8926d706970f4a997dd88e3049 - Sigstore transparency entry: 1216941988
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc9197be2bc9a0bab9137e897c033e5019d84bda77010d84ec45fb6277b2f8aa
|
|
| MD5 |
289b876ce1877ebde569dc9e5eb33943
|
|
| BLAKE2b-256 |
f26425779cc8494e41d387b7728a2ecee87c44590ad4878f7cecc82c7e43dd13
|
Provenance
The following attestation bundles were made for whiskeysour-0.1.1-cp39-abi3-win_amd64.whl:
Publisher:
release.yml on the-pro/WhiskeySour
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1-cp39-abi3-win_amd64.whl -
Subject digest:
cc9197be2bc9a0bab9137e897c033e5019d84bda77010d84ec45fb6277b2f8aa - Sigstore transparency entry: 1216942113
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type:
File details
Details for the file whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 785.0 kB
- Tags: CPython 3.9+, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4070efe3db17d25824a26e23aff13239cb6d6110b011ed9d31df0b90809c3ab0
|
|
| MD5 |
9579c8e19d12d62088994eaa03e27f1c
|
|
| BLAKE2b-256 |
caa1a9a11de935648957aed28f60fb893775efd818c3db376e97a0e84ac1c63a
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl -
Subject digest:
4070efe3db17d25824a26e23aff13239cb6d6110b011ed9d31df0b90809c3ab0 - Sigstore transparency entry: 1216942171
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type:
File details
Details for the file whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 736.5 kB
- Tags: CPython 3.9+, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae9dbb886ea66e9203025e8ce5c8b968fdd614c689dee77b3ad0537ef597656a
|
|
| MD5 |
3e2747fcebe6450af9298aa70f153b92
|
|
| BLAKE2b-256 |
fc82c20f993c59f0bd872d5e3226ed341c93b9ee350ade1f7aa2d6ef11a6a50b
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl -
Subject digest:
ae9dbb886ea66e9203025e8ce5c8b968fdd614c689dee77b3ad0537ef597656a - Sigstore transparency entry: 1216942196
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type:
File details
Details for the file whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 555.6 kB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a324457941a348a4016e35eba35f6058e00e27004f3fbe63f786aaa070e3aa00
|
|
| MD5 |
44471306ef04f4770522173ec9cadf4d
|
|
| BLAKE2b-256 |
21f2acf4cd7f86c0a41a3f2ac30ae972b529a6d9d6fb117e4d7653fc22be843d
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
a324457941a348a4016e35eba35f6058e00e27004f3fbe63f786aaa070e3aa00 - Sigstore transparency entry: 1216942027
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type:
File details
Details for the file whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 561.3 kB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7255488a5fc6c1bcf5597317e00f2d33d9dcd9e19595760f1eb44caa2ab4c1d
|
|
| MD5 |
241ba89472b6a4a2e3e99d80d9f50f75
|
|
| BLAKE2b-256 |
79ee24d29f37402490191db5bc5d98ed0ed65ec5e92edea218bbf1b4a9921d40
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
e7255488a5fc6c1bcf5597317e00f2d33d9dcd9e19595760f1eb44caa2ab4c1d - Sigstore transparency entry: 1216942076
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type:
File details
Details for the file whiskeysour-0.1.1-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: whiskeysour-0.1.1-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 508.6 kB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec04b50e879f6faafd3cf6c6bf29d4188e74c25df6ff136a0bc8cb1e34da4ed3
|
|
| MD5 |
f2f10519279311fde869ef71652204ec
|
|
| BLAKE2b-256 |
abed16c86fc7a096ab811e483406329f2e27a0c982abbf259543c555370f8af5
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1-cp39-abi3-macosx_11_0_arm64.whl -
Subject digest:
ec04b50e879f6faafd3cf6c6bf29d4188e74c25df6ff136a0bc8cb1e34da4ed3 - Sigstore transparency entry: 1216942054
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type:
File details
Details for the file whiskeysour-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: whiskeysour-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 523.3 kB
- Tags: CPython 3.9+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3404b15d694f879b21cbc5c4d40137019b2d1a6c7520fc38e2f3c0259ac25cb8
|
|
| MD5 |
75ca648e5fa7714aa2dc5b33c93f935d
|
|
| BLAKE2b-256 |
2bfcaf88a6d8e71ac7559d562832387afca7576620e4d8275bbd4838c59a971b
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whiskeysour-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl -
Subject digest:
3404b15d694f879b21cbc5c4d40137019b2d1a6c7520fc38e2f3c0259ac25cb8 - Sigstore transparency entry: 1216942142
- Sigstore integration time:
-
Permalink:
the-pro/WhiskeySour@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/the-pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3dcf6ae9a04ecaf238c545cb128ed114de9cdcf -
Trigger Event:
push
-
Statement type: