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
# 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
- 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
Metadata
Release files for whiskeysour 0.1.5
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.5.tar.gz | 50.7 kB | With provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| whiskeysour-0.1.5-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| whiskeysour-0.1.5-cp39-abi3-musllinux_1_2_x86_64.whl | CPython 3.9 | abi3 | Linux musl 1.2+ x86-64 | Details |
| whiskeysour-0.1.5-cp39-abi3-musllinux_1_2_aarch64.whl | CPython 3.9 | abi3 | Linux musl 1.2+ ARM64 | Details |
| whiskeysour-0.1.5-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.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| whiskeysour-0.1.5-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
| whiskeysour-0.1.5-cp39-abi3-macosx_10_12_x86_64.whl | CPython 3.9 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 4.3 MB
Release files / whiskeysour-0.1.5.tar.gz
| Download URL | whiskeysour-0.1.5.tar.gz |
|---|---|
| Size | 50.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dab6294fec83dd74cb1e4f50f195dc1dea59a71f629c7c030ff564332bfd4eb2
|
|
BLAKE2b-256 checksum How to use checksums |
692fc7f0ff1af0c5018d4a4318ca939fcc9528e339c71f308692066c98a1f345
|
| 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.5-cp39-abi3-win_amd64.whl
| Download URL | whiskeysour-0.1.5-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 483.0 kB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b337f8352fd8d4498b15dd3608a45e0487ecdb0828b297b537d48bc9a8f05911
|
|
BLAKE2b-256 checksum How to use checksums |
1e0050396c7d4140cc513740f5597ae6ac3a4c6ef45b8a3f57804f3cd9addce2
|
| 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.5-cp39-abi3-musllinux_1_2_x86_64.whl
| Download URL | whiskeysour-0.1.5-cp39-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 814.5 kB |
| Tags | CPython 3.9 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
f147df93a9eea69d7ca4ab851bdae575441187be2ed7f0d5463e36d5e3068f5e
|
|
BLAKE2b-256 checksum How to use checksums |
999215c8e83b1f3b2cf665cd785eb842a362d3928eb9e2439c5cdcb1b139ad49
|
| 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.5-cp39-abi3-musllinux_1_2_aarch64.whl
| Download URL | whiskeysour-0.1.5-cp39-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 764.2 kB |
| Tags | CPython 3.9 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
cc375a02b79e4b911a8c2a14958b44005606970266f65160763d4324f98c5812
|
|
BLAKE2b-256 checksum How to use checksums |
f495ff2e683f14afc383214aa63ebbf9ad2c61ce30360e94c24e177104a1f27c
|
| 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.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | whiskeysour-0.1.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 578.4 kB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
6d8ae3913dfc7fad51c23fcd6508590cb32ceb46b65eae78efc2d928f6f9eee1
|
|
BLAKE2b-256 checksum How to use checksums |
c5543b90a1f366812600c87df33b2c5de0fa588808dc7f1171ea87e016f8727e
|
| 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.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | whiskeysour-0.1.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 584.7 kB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
3bdd8b7ce46da0ed9b8d2ad0b772e40f470be224d72bf1caba2dd48c6b6a76b9
|
|
BLAKE2b-256 checksum How to use checksums |
26c65a7875fd1cdb2b6399b9829a7f1d88f430ee22272333acdb9bff33e1dc27
|
| 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.5-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | whiskeysour-0.1.5-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 523.4 kB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
e04369b3fa0290407b9bacf7448376b5737b498aa6b0c0aa7984b3d917cbadee
|
|
BLAKE2b-256 checksum How to use checksums |
6c41eee4669864272fe4ace1ce89d2ed5d6b29d7768a0235b7bcbf931f8aa317
|
| 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.5-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | whiskeysour-0.1.5-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 537.6 kB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
12e679cdc74970c5c315fdfa6f3465b80383cccc337da896e83ff2ad87cacf14
|
|
BLAKE2b-256 checksum How to use checksums |
c86c7ffb789d65cf411909eb11926ff143b3124c85e41f0be2b9f4c8460009f3
|
| 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.5
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.5.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.