rapidfuzz extractOne and scorers for fuzzy string matching: working versions (tested 2026-10-16)
Pick the best candidate with process.extractOne, control matching with WRatio, partial and token scorers, and normalize messy input with utils.default_process.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
rapidfuzz@3.14.6pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("rapidfuzz-fuzzy-match") (install), or see them on GitHub.
requirements.txt
rapidfuzz==3.14.6 pytest==9.1.1
matcher.py
from rapidfuzz import fuzz, process, utils
def best_match(query, choices, cutoff=70):
match = process.extractOne(query, choices, scorer=fuzz.WRatio, score_cutoff=cutoff)
if match is None:
return None
choice, score, index = match
return {"choice": choice, "score": score, "index": index}
def match_normalized(query, choices, cutoff=70):
return process.extractOne(
query,
choices,
scorer=fuzz.WRatio,
processor=utils.default_process,
score_cutoff=cutoff,
)
def match_reordered(query, choices):
return process.extractOne(query, choices, scorer=fuzz.token_sort_ratio)
def closest(query, choices, limit=2):
return process.extract(query, choices, scorer=fuzz.WRatio, score_cutoff=0, limit=limit)
tests/test_matcher.py
from rapidfuzz import fuzz, process, utils
from matcher import best_match, match_normalized, match_reordered, closest
CHOICES = ["New York", "Newark", "Boston"]
def test_below_cutoff_returns_none():
# extractOne yields None (not a zero score) once the cutoff is missed
assert best_match("zzzz", CHOICES, cutoff=70) is None
assert best_match("New York", CHOICES)["choice"] == "New York"
def test_wratio_beats_ratio_on_substring():
# WRatio falls back to partial matching, so the long candidate wins
assert fuzz.ratio("New York", "New York City") < fuzz.WRatio("New York", "New York City")
assert best_match("New York", ["New York City"])["score"] == 90.0
def test_default_process_normalizes_input():
# punctuation and case drop the raw score; the processor fixes it
assert fuzz.WRatio(" new york!! ", "New York") < 100
assert match_normalized(" new york!! ", CHOICES) == ("New York", 100.0, 0)
def test_token_sort_ignores_word_order():
assert fuzz.ratio("York New", "New York") < 100
assert fuzz.token_sort_ratio("York New", "New York") == 100
assert match_reordered("York New", ["New York"])[0] == "New York"
def test_mapping_returns_the_key_in_the_third_slot():
# with a dict the matched value is first and the dict key is the index slot
choice, score, key = process.extractOne(
"new york", {"nyc": "New York"}, processor=utils.default_process
)
assert choice == "New York"
assert key == "nyc"
assert choice != key
def test_extract_keeps_best_first():
results = closest("New York", CHOICES, limit=2)
assert [r[0] for r in results] == ["New York", "Newark"]
assert results[0][1] > results[1][1]
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.