rapidfuzz
RapidFuzz answers one question quickly: how similar are these two strings? The fuzz module gives you scorers like ratio, token_sort_ratio, and WRatio that return a similarity from 0 to 100. The distance module gives you the classic metrics (Levenshtein, Damerau-Levenshtein, Jaro, Jaro-Winkler, Hamming, indel, longest common subsequence, prefix, postfix), each with distance, similarity, and normalized variants plus edit operations. The matching core is C++ using bit-parallel and SIMD algorithms, and the process module runs a query against an entire list inside that C++ loop instead of a Python for loop, which is where most of the speed comes from. It began as an MIT-licensed, faster, bug-fixed rewrite of fuzzywuzzy and is now the default choice for fuzzy name, address, and product matching in Python.
The correct default for fuzzy string matching in Python: fast, MIT licensed, actively maintained, and broader in metrics than anything else at this size. Just know it measures character overlap, not meaning, and that all-pairs matching stays quadratic no matter how fast the inner loop gets.
Use it if
- You match messy human-entered strings against a known list: customer names, company names, addresses, SKU descriptions, and you need a ranked best match rather than an exact key lookup
- You are comparing thousands of strings against thousands of others and a Python-level loop over difflib is too slow; process.cdist pushes the whole cross product into C++ and can run it across cores with workers=-1
- You need a specific edit metric rather than a vague similarity: Levenshtein with custom insert/delete/substitute weights, Jaro-Winkler for short names, Hamming for fixed-length codes
- You are on fuzzywuzzy or thefuzz and want the same function names with better performance and an MIT license instead of GPL
- You need the actual edit operations, not just a score: editops and opcodes tell you which characters were inserted, deleted, or replaced so you can highlight a diff
- Your matching problem is semantic, not typographic: 'NYC' against 'New York City' or 'IBM' against 'International Business Machines' score low on every edit metric, and no scorer setting fixes that. You need an alias table or embeddings
- You are matching hundreds of thousands of records against each other. All-pairs comparison is quadratic, so 200k by 200k is 40 billion scorer calls no matter how fast each one is. You need blocking or an index first, which RapidFuzz does not provide; splink or a search engine does
- You need one distance calculation occasionally in a script: difflib.SequenceMatcher ships with Python and the standalone Levenshtein package is smaller if all you want is edit distance, so a compiled dependency is overkill
- You expect fuzzywuzzy's numbers to carry over. Since 3.0 there is no default preprocessing, so lowercase and punctuation differences now change scores, and old tutorials, blog posts, and thresholds tuned against fuzzywuzzy will quietly produce different results
- Your deployment target has no prebuilt wheel and no C++17 compiler. The pure-Python fallback loads without error and is dramatically slower, so you can ship a working but crawling service and not notice
Setup reality
pip install rapidfuzz pulls a prebuilt wheel on mainstream platforms (Linux x86_64, macOS, Windows, plus aarch64 and newer additions like risc64), so most people never see a compiler. The rough edges: on Windows a missing Visual C++ 2019 redistributable produces 'ImportError: DLL load failed'; building from source needs a C++17 compiler and a recursive git clone because the C++ core is a submodule; and if the compiled extension cannot load, a pure-Python fallback takes over silently at a fraction of the speed. process.cdist and cpdist return NumPy arrays and require NumPy, which is not a hard dependency, so a fresh environment can import rapidfuzz fine and then fail on the first cdist call. The supported Python window also moves fast: 3.9 was dropped in 3.14.0 and 3.10 is being dropped next, so pinning matters on older runtimes. Finally, remember that as of 3.0 nothing is preprocessed unless you pass processor=utils.default_process.
Patterns
Score two strings from 0 to 100basic-similarity-score
from rapidfuzz import fuzz
fuzz.ratio("this is a test", "this is a test!")
# 96.55172413793103
fuzz.partial_ratio("this is a test", "say this is a test today")
# 100.0ratio compares the whole strings; partial_ratio finds the best matching substring window, so a short query inside a long string still scores 100. Both return floats, not ints.
Lowercase and strip punctuation before scoringnormalize-before-comparing
from rapidfuzz import fuzz, utils
fuzz.WRatio("this is a word", "THIS IS A WORD")
# 21.42857142857143
fuzz.WRatio("this is a word", "THIS IS A WORD",
processor=utils.default_process)
# 100.0Since RapidFuzz 3.0 nothing is preprocessed by default. default_process lowercases, replaces non-alphanumerics with spaces, and trims. Skipping it is the single most common cause of surprisingly low scores.
Handle reordered and duplicated wordspick-a-token-scorer
from rapidfuzz import fuzz
fuzz.ratio("fuzzy wuzzy was a bear", "wuzzy fuzzy was a bear")
# 90.9090909090909
fuzz.token_sort_ratio("fuzzy wuzzy was a bear", "wuzzy fuzzy was a bear")
# 100.0
fuzz.token_set_ratio("fuzzy was a bear but not a dog", "fuzzy was a bear")
# 100.0token_sort_ratio sorts words before comparing, so order stops mattering. token_set_ratio returns 100 whenever one token set is a subset of the other, which makes it dangerously permissive for short queries against long catalog entries.
Find the single best match in a listbest-match-from-list
from rapidfuzz import process, fuzz, utils
choices = ["Atlanta Falcons", "New York Jets",
"New York Giants", "Dallas Cowboys"]
process.extractOne("cowboys", choices,
scorer=fuzz.WRatio,
processor=utils.default_process,
score_cutoff=80)
# ('Dallas Cowboys', 90.0, 3)The third tuple element is the index for a list and the key for a dict. With score_cutoff set, extractOne returns None when nothing clears the bar, so always handle None rather than unpacking blindly.
Get the top N candidates with a thresholdtop-n-matches
from rapidfuzz import process, fuzz, utils
results = process.extract(
"new york jets", choices,
scorer=fuzz.WRatio,
processor=utils.default_process,
limit=3,
score_cutoff=60,
)
# [('New York Jets', 100.0, 1), ('New York Giants', 78.57142857142857, 2)]Results come back sorted best first and shorter than limit when score_cutoff filters entries out. Use process.extract_iter instead if choices is a huge generator you do not want to rank in full.
Score every query against every choice with cdistcross-product-matrix
import numpy as np
from rapidfuzz import process, fuzz
queries = ["aaa", "bbb", "ccc"]
choices = ["aab", "bbc", "ccd", "xyz"]
matrix = process.cdist(
queries, choices,
scorer=fuzz.ratio,
workers=-1,
dtype=np.uint8,
)
best_idx = matrix.argmax(axis=1)cdist keeps the whole double loop in C++ and workers=-1 spreads it over all cores, which is far faster than nesting extractOne in Python. It needs NumPy installed and allocates len(queries) by len(choices) cells, so cap the size or use dtype=np.uint8.
Compare two lists element by element with cpdistpairwise-column-compare
from rapidfuzz import process, fuzz
left = ["Jon Smith", "Jane Doe", "Bob Ray"]
right = ["John Smith", "Jane Doe", "Robert Ray"]
scores = process.cpdist(left, right, scorer=fuzz.WRatio)
# array of 3 scores, one per row paircpdist is row-wise, not a matrix: use it to score two aligned dataframe columns. Passing lists of different lengths is an error, unlike cdist which happily builds a rectangle.
Use a specific edit distance instead of a fuzz scoreedit-distance-metrics
from rapidfuzz.distance import Levenshtein, JaroWinkler, DamerauLevenshtein
Levenshtein.distance("kitten", "sitting") # 3
Levenshtein.normalized_similarity("kitten", "sitting") # 0.5714...
Levenshtein.distance("kitten", "sitting", weights=(1, 1, 2)) # 5
JaroWinkler.similarity("MARTHA", "MARHTA") # 0.961...
DamerauLevenshtein.distance("CA", "AC") # 1Every metric exposes distance, similarity, normalized_distance, and normalized_similarity. weights is (insertion, deletion, substitution); setting substitution to 2 gives you the indel distance that fuzz.ratio is built on.
Let the cutoff short-circuit the algorithmscore-cutoff-speedup
from rapidfuzz.distance import Levenshtein
from rapidfuzz import fuzz
Levenshtein.distance("a very long string here", "totally different",
score_cutoff=3)
# 4 (cutoff + 1, computation abandoned early)
fuzz.ratio("abcdef", "uvwxyz", score_cutoff=80)
# 0.0score_cutoff is a performance feature, not just a filter: the C++ code bails out as soon as the result cannot beat the cutoff. Distance functions return cutoff + 1 and similarity functions return 0 rather than the true value, so never store a cutoff result as the real score.
Get the actual edits between two stringsedit-operations
from rapidfuzz.distance import Levenshtein
ops = Levenshtein.editops("spam", "eggs")
for op in ops:
print(op.tag, op.src_pos, op.dest_pos)
# replace 0 0 ... etc
print(ops.as_opcodes())
print(ops.apply("spam", "eggs")) # 'eggs'editops gives one entry per change; as_opcodes gives difflib-style blocks including equal runs, which is what you want for rendering a highlighted diff. Computing editops is slower than computing distance alone, so do not call it inside a hot ranking loop.
Find where the best partial match starts and endslocate-partial-match
from rapidfuzz import fuzz
alignment = fuzz.partial_ratio_alignment(
"invoice number",
"see attached invoice numbr for details",
)
print(alignment.score, alignment.dest_start, alignment.dest_end)partial_ratio only tells you how well the query fits somewhere; partial_ratio_alignment tells you where, so you can slice the original text. It returns None when the score is below score_cutoff.
Pass metric options through the process helperscustom-scorer-kwargs
from rapidfuzz import process
from rapidfuzz.distance import JaroWinkler
process.extractOne(
"MARHTA", ["MARTHA", "MARY", "MARTIN"],
scorer=JaroWinkler.normalized_similarity,
scorer_kwargs={"prefix_weight": 0.2},
score_cutoff=0.9,
)Distance-module scorers return 0 to 1, not 0 to 100, so a score_cutoff copied from a fuzz-based call will silently reject everything. scorer_kwargs is the only supported way to configure the scorer; a lambda wrapper forces the slow Python path.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| Levenshtein | PyPI | You only need raw edit distance and ratio functions and want a smaller surface area; it is built on the same C++ core by the same author. |
| jellyfish | PyPI | You need phonetic matching (Soundex, Metaphone, NYSIIS) alongside edit distances, which RapidFuzz deliberately does not include. |
| thefuzz | PyPI | You are maintaining legacy fuzzywuzzy code and want the old import paths; it now wraps RapidFuzz underneath anyway. |
| splink | PyPI | You are linking large record sets and need blocking plus probabilistic scoring, not a single pairwise similarity number. |