WhiskeySour
A high-performance drop-in replacement for Python's BeautifulSoup, written in Rust and published as a native Python package via PyO3.
Status: stable — 561 unit, 58 BS4-parity integration and 16 property-based fuzz tests passing.
Why WhiskeySour?
BeautifulSoup is beloved but slow: every node is a Python object and all parsing and matching runs in Python. WhiskeySour keeps the same API and moves the tree, the parser and the query engine into Rust.
Measured with a release build (maturin develop --release) against bs4 4.15 + html.parser, Python 3.14, Apple Silicon. Query rows use the 100 KB document; each figure is the best of three medians.
| Operation | WhiskeySour | bs4 + html.parser | Speedup |
|---|---|---|---|
| Parse 10 KB | 105 µs | 1.42 ms | 14× |
| Parse 100 KB | 1.04 ms | 14.49 ms | 14× |
| Parse 500 KB | 5.27 ms | 73.07 ms | 14× |
find(id=…) |
8.29 µs | 636 µs | 77× |
find_all(class_=…) |
34 µs | 1.31 ms | 39× |
find_all(string=…) |
14 µs | 533 µs | 38× |
select("div.item") |
49 µs | 1.43 ms | 29× |
select("div:nth-child(2n)") |
38 µs | 2.18 ms | 58× |
select_one(…) on 100 items |
16 µs | 516 µs | 33× |
find(…) on 100 items |
37 µs | 241 µs | 6.6× |
get_text() |
14 µs | 195 µs | 14× |
str() (serialize) |
69 µs | 7.74 ms | 113× |
prettify() |
75 µs | 8.88 ms | 119× |
iterate .descendants |
491 µs | 111 µs | bs4 ~4.4× faster¹ |
list(.strings) |
228 µs | 186 µs | bs4 ~1.2× faster¹ |
tag.get("class") |
0.13 µs | 0.04 µs | bs4 ~3.1× faster¹ |
| Memory per node | ~210–250 bytes | ~650 bytes | ~3× less |
¹ bs4's tree is a tree of Python objects, so walking it node by node or reading one attribute is a plain attribute/dict access. WhiskeySour has to cross into Rust and build a Python wrapper for each node it hands back (the Rust walk itself takes ~2.4 ns per node). When you need many nodes, let Rust do the filtering — find_all, select, get_text(), find_all(string=…) — rather than iterating .descendants / .strings in Python.
Regenerate this table with python tests/python/performance/readme_table.py and the memory row with pytest tests/python/performance/bench_memory.py -s. For an HTML report across five document shapes, run python tests/python/performance/bench_comparison.py.
Key implementation choices:
- Rust core via PyO3 + maturin
- html5ever — spec-compliant HTML5 parser (from the Servo project)
- Compact arena — the whole tree is one flat
Vecof 56-byte nodes addressed byu32ids (element data is boxed, links are 4 bytes); traversal follows index links without allocating - Hand-written CSS selector engine — parsed selectors are cached per thread, and
:nth-*indices are memoised per query so long lists stay linear - GIL release — parsing, and queries or serialisation over large subtrees, run outside the Python GIL, so threads can work on documents concurrently
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)
# Reusable CSS selector — the parsed selector is cached, so repeated use skips re-parsing
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/
│ ├── whiskysour-core/ # Pure Rust library (no Python deps)
│ │ └── src/
│ │ ├── parser/ # html5ever integration
│ │ ├── node.rs # Arena-allocated node pool
│ │ ├── selector/ # CSS selector parser, matcher + per-thread cache
│ │ ├── traversal/ # Tree iterators
│ │ ├── query/ # find() / find_all() / select()
│ │ └── serialize/ # HTML serialisation + prettify
│ │ └── benches/ # Criterion benchmarks (parse, find, select, serialize)
│ │
│ └── whiskysour-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/ # 561 tests across 11 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; build with --release first).
# Includes per-item navigation benchmarks and a 4-thread scaling check.
pytest tests/python/performance/ --override-ini="addopts=" -q
# Memory usage vs bs4 (peak RSS in a fresh process; POSIX only)
pytest tests/python/performance/bench_memory.py --override-ini="addopts=" -q -s
# Rust-level Criterion benchmarks (parse, find_all/find_one, tree walk,
# get_text, select incl. :nth-*, serialize/prettify)
cargo bench -p whiskysour-core
# 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, void elements |
unit/test_malformed_html.py |
52 | Broken markup recovery, misnesting, stray tags |
unit/test_encoding.py |
31 | UTF-8/16, Latin-1, BOM, meta charset, surrogate pairs |
unit/test_find.py |
85 | find/find_all by tag/id/class/attr/string/regex/lambda, limit |
unit/test_css_selectors.py |
83 | CSS3 + :has/:is/:where, structural pseudo-classes, limit, compiled selectors |
unit/test_tree_navigation.py |
76 | parent/children/siblings/descendants/.string/.strings/get_text |
unit/test_modification.py |
49 | decompose/extract/replace_with/insert/append/wrap |
unit/test_output.py |
47 | str()/prettify()/encode()/escaping/round-trip stability |
unit/test_edge_cases.py |
45 | 10k+ nodes, deep nesting, concurrency (incl. queries racing mutation), control chars |
unit/test_streaming.py |
19 | StreamParser push API, parse_stream() generator |
unit/test_review_fixes.py |
7 | Regression tests for reviewed bugs |
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 whiskysour-py
# Dev build (fast recompile, debug symbols)
maturin develop
# Release build (use for perf work)
maturin develop --release
# Rust tests
cargo test
# Formatting / linting (clippy warnings are errors)
cargo fmt
cargo clippy --all-targets -- -D warnings
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 clippy --all-targets -- -D warningsfor Rust changes - Performance regressions > 5% against the baseline will block merge — benchmark with a release build
Licence
MIT
Metadata
Release files for whiskeysour 0.1.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| whiskeysour-0.1.6.tar.gz | 56.6 kB | With provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| whiskeysour-0.1.6-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| whiskeysour-0.1.6-cp39-abi3-musllinux_1_2_x86_64.whl | CPython 3.9 | abi3 | Linux musl 1.2+ x86-64 | Details |
| whiskeysour-0.1.6-cp39-abi3-musllinux_1_2_aarch64.whl | CPython 3.9 | abi3 | Linux musl 1.2+ ARM64 | Details |
| whiskeysour-0.1.6-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| whiskeysour-0.1.6-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| whiskeysour-0.1.6-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
| whiskeysour-0.1.6-cp39-abi3-macosx_10_12_x86_64.whl | CPython 3.9 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 4.4 MB
Release files / whiskeysour-0.1.6.tar.gz
| Download URL | whiskeysour-0.1.6.tar.gz |
|---|---|
| Size | 56.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a126721b5a5324218435db9bccdcfd75f8ff3b4234cc5a282a06b037f9badfc
|
|
BLAKE2b-256 checksum How to use checksums |
d62777e4ff407036cd9937226559aa2d8281dd6b9b164131edd68e6fe7a79083
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / whiskeysour-0.1.6-cp39-abi3-win_amd64.whl
| Download URL | whiskeysour-0.1.6-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 489.3 kB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
7e0cd308a5b90a7b1e2e25cfb9715b2eaf9863e2132ed7a3e1a02da943707be0
|
|
BLAKE2b-256 checksum How to use checksums |
f97bc54dcc465f96744125f6e23f4ec62dc775cc04c5b8b668e2360788b8f8f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / whiskeysour-0.1.6-cp39-abi3-musllinux_1_2_x86_64.whl
| Download URL | whiskeysour-0.1.6-cp39-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 817.0 kB |
| Tags | CPython 3.9 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b0fb6f18bd8e5d27ba4f23790df3d6b34f908b7ddc5b1f0d0a1bb76996baa7e5
|
|
BLAKE2b-256 checksum How to use checksums |
3b7a5ebbe9bca5736b1a250e45711bb778aa9d5a84d5aceb0b380647bb58536e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / whiskeysour-0.1.6-cp39-abi3-musllinux_1_2_aarch64.whl
| Download URL | whiskeysour-0.1.6-cp39-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 768.8 kB |
| Tags | CPython 3.9 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
0cd579cba89dedc4398028ca52bf11777c7d72550b1a88bf6aa28394d296aa52
|
|
BLAKE2b-256 checksum How to use checksums |
08e712840a63bd26540e591c2723821d8421de7de20fd6fc3f82975ae84666a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / whiskeysour-0.1.6-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | whiskeysour-0.1.6-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 581.1 kB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
a8cb1e9ebfb6cc51f02b2cc04a244af2b4b17a2e9bfcb64686b8b5580768bea5
|
|
BLAKE2b-256 checksum How to use checksums |
95e394d49a9726d4d3c910d852f77f4ea39a2c7db1a4f90f0070727036b5c46a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / whiskeysour-0.1.6-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | whiskeysour-0.1.6-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 589.8 kB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
2a02787666b020f24a4c7c01d23f26edf441fd12bd6f24124f67910600bb40e9
|
|
BLAKE2b-256 checksum How to use checksums |
47c27d9d4a2c536ec704f7e4bec09d0856284a48075ebe5a5906e6e19d4a2d77
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / whiskeysour-0.1.6-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | whiskeysour-0.1.6-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 527.8 kB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
9196336d43f24ff8e8c59578bb6982a64297324ed7781bfc644e8ed287924921
|
|
BLAKE2b-256 checksum How to use checksums |
3261a3be2440cfb4108cf424dbc00f87d54947c26f0f01b99c779481cb16e75a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / whiskeysour-0.1.6-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | whiskeysour-0.1.6-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 541.6 kB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
20253b82859f469ab28fdec06a869e49061d5f00193dc93e61ade6b0ee338c63
|
|
BLAKE2b-256 checksum How to use checksums |
d8b80a7d86139dc6afff8532132cf0c96137cd7ed23cda0a4d8735f964157a25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logSecurity for whiskeysour 0.1.6
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 8 files in this release have been uploaded with provenance metadata.
Attestation for whiskeysour-0.1.6.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.