mrkeyoor.com_
Thu 06 Aug 02:42 UTC
PyPIUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability4/5The 3.x line has been stable since 2023 and changes within it are additive, but 3.0 removed default preprocessing, which silently changed everyone's scores, and each minor release drops an old Python version.
Docs4/5rapidfuzz.github.io covers every scorer and metric with signatures, examples, and published benchmarks, plus an api_differences page against fuzzywuzzy; guidance on picking between token_sort and token_set for a real dataset is thinner.
Maintenance4/5Pushed August 2026 with 29 open issues (31 counting PRs), regular releases including free-threaded Python wheels, but it is essentially one maintainer funded by sponsorship.
Ecosystem4/5Around 43 million weekly downloads, and it is the engine under thefuzz and the standalone Levenshtein package; it stays a leaf utility rather than a platform with plugins.

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
Skip it if

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.0

ratio 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.0

Since 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.0

token_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 pair

cpdist 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")              # 1

Every 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.0

score_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

PackageRegistryPick it when
LevenshteinPyPIYou 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.
jellyfishPyPIYou need phonetic matching (Soundex, Metaphone, NYSIIS) alongside edit distances, which RapidFuzz deliberately does not include.
thefuzzPyPIYou are maintaining legacy fuzzywuzzy code and want the old import paths; it now wraps RapidFuzz underneath anyway.
splinkPyPIYou are linking large record sets and need blocking plus probabilistic scoring, not a single pairwise similarity number.