StringZilla for Python wraps SIMD- and SWAR-accelerated native kernels behind str- and bytes-shaped types that avoid copies wherever possible.
The stringzilla module covers single-string search, slicing, splitting, trimming, translation, hashing, checksums, sorting, sampling, random generation, UTF-8 segmentation, Unicode case-folding, and Unicode normalization.
The companion stringzillas module covers batch-parallel edit distances, alignment scores, and rolling fingerprints spread across CPU cores and CUDA devices.
Every kernel selects the fastest backend for the running CPU at import time, and the batched engines release the GIL around native work.
The module is also marked safe for free-threaded Py_GIL_DISABLED CPython builds.
Throughout this document the stringzilla package is imported as sz, the stringzillas package as szs, and NumPy as np:
import stringzilla as sz
import stringzillas as szs
import numpy as npAlmost every sz operation is available both as a method on a Str object and as a module-level function that accepts a str, bytes, bytearray, or Str.
For example sz.Str("hello").find("l") and sz.find("hello", "l") are equivalent.
StringZilla ships on PyPI as the single-string module plus an optional batch-engine package:
pip install stringzilla # single-string kernels: the `stringzilla` module
pip install stringzillas-cpus # batch engines on CPU cores: the `stringzillas` module
pip install stringzillas-cuda # batch engines on NVIDIA GPUs: the `stringzillas` moduleInstall stringzilla for the single-string API.
Install exactly one of stringzillas-cpus or stringzillas-cuda for the batch engines; both expose the same import stringzillas package, differing only in whether CUDA kernels are compiled in.
The batch engines accept sz.Strs collections and NumPy arrays, so NumPy is required to use them.
The stringzilla module exposes six public types: Str, Strs, File, the incremental hashers Hasher, Sha256 and Sha256s, plus a family of UTF-8 iterator types.
Str(source) is an immutable, zero-copy view over a str, bytes, bytearray, another Str, or a memory-mapped File.
It implements the read-only half of the str/bytes protocol — indexing, slicing, iteration, len, in, rich comparison, hashing, and the buffer protocol — without materializing new Python objects.
Slicing returns a new Str view into the same backing buffer rather than copying bytes.
Two read-only properties expose its memory layout for zero-copy interop with PyArrow, ctypes, and similar:
Str.address— integer memory address of the first byte.Str.nbytes— length of the view in bytes.
import stringzilla as sz
text = sz.Str("the quick brown fox")
assert str(text[4:9]) == "quick" # a zero-copy view
assert str(text[10:15]) == "brown"
assert "quick" in text
assert len(text) == 19
assert text.nbytes == 19
assert isinstance(text.address, int)Strs(sequence, view=False) is an ordered, indexable, sliceable collection of Str views.
It is produced by the splitting and sorting methods, and can also be built directly from a list, tuple, generator, or a pyarrow.Array.
With view=True it references the original data instead of copying it.
Strs keeps its parts on a single contiguous "tape" with an offsets array, so sorted, argsort, intersect, sample, and shuffled reorder offsets rather than bytes.
This layout is also dramatically more memory-efficient than a Python list of str.
Every part lives on one shared byte tape alongside a compact offsets array, so N substrings cost roughly one allocation, whereas a list of str holds N separately heap-allocated objects, each carrying a PyObject header, a hash cache, and per-object allocator overhead.
Splitting a large document into millions of tokens therefore stays compact and cache-friendly instead of fragmenting the heap.
The lazy split_iter, rsplit_iter, and *_byteset_iter forms go a step further and build no collection at all, yielding one Str view at a time.
It carries one of three internal storage layouts:
TAPE— owns a contiguous data buffer with an offsets array; StringTape-compatible.TAPE_VIEW— a zero-copy view into existing data, such as an Arrow or StringTape slice.FRAGMENTED— non-contiguous strings with individual pointers.
Several read-only properties expose the tape for Apache Arrow / StringTape interop:
| Property | Meaning |
|---|---|
Strs.tape |
In-place transform to the Arrow layout. |
Strs.tape_address |
Address of the tape buffer's first byte. |
Strs.tape_nbytes |
Total tape length in bytes. |
Strs.offsets_address |
Address of the offsets array's first byte. |
Strs.offsets_nbytes |
Offsets array length in bytes. |
Strs.offsets_are_large |
True when 64-bit offsets are needed. |
Strs.__layout__ |
Internal layout name. |
Strs.tape rewrites the collection in place into the Arrow string layout — a contiguous buffer plus an offsets array.
Strs.offsets_are_large is True when the tape is too large for 32-bit offsets and needs 64-bit ones for Arrow export.
Strs.__layout__ is a debug string reporting the internal storage: TAPE, TAPE_VIEW, or FRAGMENTED.
import stringzilla as sz
names = sz.Strs(["banana", "apple", "cherry", "date"])
assert len(names) == 4
assert str(names[0]) == "banana"
assert [str(x) for x in names[1:3]] == ["apple", "cherry"] # a sliced view
names.__layout__ # e.g. 'TAPE'File(path, mode='r') memory-maps a path so a Str can view data larger than RAM without reading it into memory.
The default mode is 'r', for read-only access.
import os, tempfile, pathlib
import stringzilla as sz
path = os.path.join(tempfile.mkdtemp(), "log.txt")
pathlib.Path(path).write_text("error: disk full\nok\nerror: timeout\n")
mapped = sz.Str(sz.File(path)) # scan in place, no copy into RAM
assert mapped.count("error") == 2All search methods accept optional start and end indices that bound the search window; start defaults to 0 and end to the string length.
They are available both as Str methods and as module-level functions.
find(substring, start=0, end=len)— index of the first occurrence, or-1if absent.rfind(substring, start=0, end=len)— index of the last occurrence, or-1.index(substring, start=0, end=len)— likefind, but raisesValueErrorif absent.rindex(substring, start=0, end=len)— likerfind, but raisesValueErrorif absent.contains(substring, start=0, end=len)—True/Falsemembership test.startswith(prefix, start=0, end=len)—Trueif the windowed string starts withprefix.endswith(suffix, start=0, end=len)—Trueif the windowed string ends withsuffix.count(substring, start=0, end=len, allowoverlap=False)— number of occurrences; passallowoverlap=Trueto count overlapping matches.
import stringzilla as sz
text = sz.Str("the quick brown fox")
assert text.find("brown") == 10
assert text.rfind("o") == 17
assert text.index("quick") == 4
assert text.contains("quick")
assert text.startswith("the")
assert text.endswith("fox")
assert sz.Str("banana").count("a") == 3
assert sz.Str("aaaa").count("aa") == 2 # non-overlapping
assert sz.Str("aaaa").count("aa", allowoverlap=True) == 3 # overlappingThe *_of / *_not_of family searches for any byte from a set, the byteset-accelerated counterpart of substring search.
count_byteset counts how many bytes fall in the set.
find_first_of(chars, start=0, end=len)— index of the first byte that is inchars, or-1.find_last_of(chars, start=0, end=len)— index of the last byte that is inchars, or-1.find_first_not_of(chars, start=0, end=len)— index of the first byte not inchars, or-1.find_last_not_of(chars, start=0, end=len)— index of the last byte not inchars, or-1.count_byteset(chars, start=0, end=len)— number of bytes that are inchars.
import stringzilla as sz
text = sz.Str("hello world")
assert text.find_first_of("aeiou") == 1
assert text.find_last_of("aeiou") == 7
assert text.find_first_not_of("he") == 2
assert text.find_last_not_of("lo") == 10
assert text.count_byteset("lo") == 5sz.equal(first, second)— module-level fast byte-wise equality, returningbool.offset_within(larger)— returns the byte offset of thisStrview inside a largerStrit was sliced from, or-1if it is not a sub-view.
import stringzilla as sz
assert sz.equal("abc", "abc")
big = sz.Str("hello world")
assert big[6:].offset_within(big) == 6split(separator, maxsplit=∞, keepseparator=False, skip_empty=False)— split on a non-empty substring separator; raisesValueErroron an empty separator.rsplit(separator, maxsplit=∞, keepseparator=False, skip_empty=False)— same, scanning from the right.split_byteset(separators, maxsplit=∞, keepseparator=False, skip_empty=False)— split on any single byte from theseparatorsset.rsplit_byteset(separators, maxsplit=∞, keepseparator=False, skip_empty=False)— same, from the right.splitlines(keeplinebreaks=False, maxsplit=∞)— split on line breaks.
maxsplit caps the number of splits and defaults to unlimited.
keepseparator keeps the matched separator attached to the parts.
skip_empty drops empty segments between adjacent separators.
import stringzilla as sz
assert list(map(str, sz.Str("a,b,c").split(","))) == ["a", "b", "c"]
assert list(map(str, sz.Str("a,b,c").rsplit(",", maxsplit=1))) == ["a,b", "c"]
assert list(map(str, sz.Str("a,b;c").split_byteset(",;"))) == ["a", "b", "c"]
assert list(map(str, sz.Str("a\nb\nc").splitlines())) == ["a", "b", "c"]Each eager split has a lazy counterpart that yields Str views one at a time without building the whole Strs.
These take keepseparator=False and skip_empty=False but no maxsplit.
split_iter(separator, keepseparator=False, skip_empty=False)rsplit_iter(separator, keepseparator=False, skip_empty=False)split_byteset_iter(separators, keepseparator=False, skip_empty=False)rsplit_byteset_iter(separators, keepseparator=False, skip_empty=False)
The reverse iterators yield the last field first.
import stringzilla as sz
for field in sz.Str("2024-01-15").split_iter("-"):
print(field) # 2024, then 01, then 15
assert str(next(iter(sz.Str("a/b/c").rsplit_iter("/")))) == "c"
assert sum(1 for _ in sz.Str("a,b,c").split_iter(",")) == 3partition(separator)— return a 3-tuple(head, separator, tail)split at the first occurrence; if not found returns(self, '', '').rpartition(separator)— same, at the last occurrence; if not found returns('', '', self).
import stringzilla as sz
assert tuple(map(str, sz.Str("a=b=c").partition("="))) == ("a", "=", "b=c")
assert tuple(map(str, sz.Str("a=b=c").rpartition("="))) == ("a=b", "=", "c")strip(chars=whitespace)— remove leading and trailing bytes that are inchars.lstrip(chars=whitespace)— remove only leading bytes.rstrip(chars=whitespace)— remove only trailing bytes.
With no argument, the default character set is ASCII whitespace. Pass a byteset to trim an arbitrary set of bytes.
import stringzilla as sz
assert sz.Str(" hi ").strip() == "hi"
assert sz.Str("xxhi").lstrip("x") == "hi"
assert sz.Str("hi!!").rstrip("!") == "hi"translate(table, inplace=False, start=0, end=len) applies a byte-to-byte mapping in a single pass.
table is either a 256-byte string/bytes lookup table or a dict mapping bytes to bytes.
With inplace=True the buffer is rewritten in place and None is returned; otherwise a new bytes/str is returned.
A non-256-byte table raises ValueError; a non-string, non-dict table raises TypeError.
import stringzilla as sz
assert sz.Str("abc").translate({"a": "A"}) == b"Abc"
table = bytes(range(256)).translate(bytes.maketrans(b"abc", b"ABC"))
assert sz.Str("cabbage").translate(table) == b"CABBAge"decode(encoding='utf-8', errors='strict') decodes the bytes to a Python str, mirroring bytes.decode.
import stringzilla as sz
assert sz.Str("abc").decode() == "abc"sz.hash(text, seed=0)— seeded 64-bit AES-accelerated hash, returned as an unsignedint. This differs from Python's built-inhash(), which returns a platform-dependentPy_hash_t.sz.hash_multiseed(text, seeds, out=None)— hash one string under many seeds at once.seedsis a contiguous buffer ofuint64, such as anumpy.uint64array orarray('Q', ...); plainintlists are not accepted. Returns a tuple of ints, or fills the optional contiguousuint64outbuffer in place and returnsNone. This is much faster than loopinghashfor short strings under many seeds, and is useful for feature hashing, Count-Min sketches, Bloom/cuckoo filters, and MinHash/LSH.sz.bytesum(text)— additive checksum of the individual byte values, as anint.sz.sha256(text)— 32-byte SHA-256 digest asbytes.sz.hmac_sha256(key, message)— 32-byte HMAC-SHA256 digest asbytes.
hash, hash_multiseed, bytesum, and sha256 are also available as Str methods.
import stringzilla as sz
import numpy as np
sz.hash("hello") # seeded 64-bit value
sz.hash("hello", seed=42) # a different value
assert sz.bytesum("abc") == 294
assert sz.sha256("abc").hex()[:8] == "ba7816bf"
assert len(sz.hmac_sha256(b"key", b"message")) == 32
seeds = np.arange(8, dtype=np.uint64)
assert len(sz.hash_multiseed("hello", seeds)) == 8 # tuple of 8 intsHasher(seed=0) is an incremental, AES-accelerated hasher producing the same 64-bit value as sz.hash.
update(data)— absorb astr/byteschunk; returnsselffor chaining.digest()— current hash as an unsigned 64-bitint, without consuming state.hexdigest()— current hash as a 16-character lowercase hexstr.reset()— reset to the initial seed; returnsself.
import stringzilla as sz
h = sz.Hasher()
h.update(b"hello").update(b" world")
assert h.digest() == sz.hash(b"hello world")
assert len(h.hexdigest()) == 16Sha256() is an incremental SHA-256 hasher with a hashlib-compatible interface, hardware-accelerated where available.
update(data)— absorb astr/byteschunk; returnsself.digest()— current 32-byte digest asbytes.hexdigest()— current digest as a 64-character lowercase hexstr.reset()— reset to the initial SHA-256 constants; returnsself.copy()— return an independent copy with identical internal state.
import stringzilla as sz
import hashlib
assert sz.Sha256().update(b"abc").hexdigest() == hashlib.sha256(b"abc").hexdigest()
a = sz.Sha256().update(b"ab")
assert a.copy().update(b"c").hexdigest() == sz.Sha256().update(b"abc").hexdigest()Digesting one message is a serial dependency chain, but independent messages compress in parallel lanes — sixteen at a time on AVX-512, eight on AVX2.
Sha256s(lanes) holds one hasher per lane, with update(chunks) taking exactly one chunk per lane and digest(out=None), hexdigest(), reset(), copy() and len() behaving as on Sha256.
Lanes are workers rather than messages: lanes[i] reads one as a Sha256, and lanes[i] = sz.Sha256() retires a finished lane while the others keep streaming.
sz.hmac_sha256(key, message, out=None) takes either one message or a collection, authenticating a batch of tokens sharing a secret through the same kernels.
Both accept a Strs, read in place whatever its layout, and an out buffer of bytes — a (count, 32) numpy.uint8 matrix receives one digest per row with no allocation, since that is byte-for-byte the layout the kernel writes.
The GIL is released around the kernel, so several threads digest concurrently.
Lanes advance in lockstep within a group, so a group costs as much as its longest message.
Every length is handled correctly, but throughput is best when messages of similar length share a group, which Strs.argsort arranges.
import numpy as np
import stringzilla as sz
texts = sz.Strs(["alpha", "beta", "gamma"])
lanes = sz.Sha256s(len(texts))
digests = np.empty((len(lanes), sz.Sha256.digest_length), np.uint8)
lanes.update(texts).digest(out=digests)
tags = sz.hmac_sha256(b"secret", texts) # one 32-byte tag per message
assert tags[0] == sz.hmac_sha256(b"secret", "alpha")These operate on a Strs collection and return new collections or index tuples, leaving the original unchanged.
sorted(reverse=False, uncased=False, top=None)— return a new, stably sortedStrs.reversesorts descending,uncasedorders by Unicode case-folding, andtopkeeps only the leadingtopelements (a partial top-k, cheaper than a full sort).argsort(reverse=False, uncased=False, top=None, out=None)— return the stable permutation of indices that sorts the collection, as a tuple of ints.outis an optional writable, C-contiguous 64-bit-unsigned buffer such asnumpy.uintporarray('Q'), receiving the indices with zero allocation; when given,outitself is returned.
import stringzilla as sz
names = sz.Strs(["banana", "apple", "cherry"])
assert [str(x) for x in names.sorted()] == ["apple", "banana", "cherry"]
assert [str(x) for x in names.sorted(reverse=True)] == ["cherry", "banana", "apple"]
assert [str(x) for x in names.sorted(top=2)] == ["apple", "banana"]
assert names.argsort() == (1, 0, 2)intersect(other, seed=0)— return the positions of strings present in both collections, as a pair of parallel index tuples:result[0][i]in this collection andresult[1][i]inotherpoint to equal strings. Each distinct shared value is matched exactly once, even if either side holds duplicates;seedreshuffles the underlying hash table to resist adversarial inputs.
import stringzilla as sz
ours, theirs = sz.Strs(["banana", "apple", "cherry"]).intersect(sz.Strs(["cherry", "orange", "banana"]))
assert sorted(ours) == [0, 2] # "banana" and "cherry" on our side
assert len(ours) == len(theirs) == 2sample(size, seed=None)— return a newStrsofsizeelements drawn at random with replacement;seedmakes it reproducible.shuffled(seed=None)— return a newStrswith the elements randomly permuted;seedmakes it reproducible.
import stringzilla as sz
pool = sz.Strs(["a", "b", "c", "d"])
assert len(pool.sample(2)) == 2
assert [str(x) for x in sorted(pool.shuffled(seed=42))] == ["a", "b", "c", "d"]These module-level functions generate random bytes and are not tied to Strs:
sz.random(length, nonce=0, alphabet=None)— return a fresh randombytesoflength; ifalphabetis given, each bytebis mapped toalphabet[b % len(alphabet)].sz.fill_random(buffer, nonce=0, alphabet=None, start=0, end=len)— fill a writable, contiguous byte buffer such as abytearray,memoryview, orStrin place with pseudo-random bytes, optionally remapped toalphabet, optionally limited to the[start, end)slice. ReturnsNone. Also available as theStr.fill_randommethod.
import stringzilla as sz
assert len(sz.random(16)) == 16
sz.random(8, alphabet="ACGT") # e.g. b'GCTAACGT'
buf = bytearray(16)
sz.fill_random(buf) # mutates buf in place
assert len(buf) == 16The stringzillas module scores many string pairs at once across CPU cores or a CUDA device.
Each engine is constructed once with its cost model, then called over two collections to produce a dense cross-product result matrix.
DeviceScope(cpu_cores=None, gpu_device=None) controls where work runs.
Pass cpu_cores=N to restrict to N CPU cores, where 0 means all cores, or gpu_device=ID to target a CUDA device; you cannot pass both.
A DeviceScope may be supplied at construction time as the capabilities argument, and/or per call as the device argument.
import stringzillas as szs
cpu_scope = szs.DeviceScope(cpu_cores=4)
gpu_scope = szs.DeviceScope(gpu_device=0) # requires stringzillas-cudaEvery engine is called as engine(queries, candidates=None, device=None, out=None):
queries— the collection forming the matrix rows, either asz.Strsor any string sequence.candidates— the collection forming the matrix columns; when omitted orNone, the engine computes the symmetric self-similarity ofqueries.device— an optionalDeviceScopeoverriding the constructor's.out— an optional pre-allocated 2-D NumPy output buffer of shape(len(queries), len(candidates)).
The result result[i, j] is the distance or score between queries[i] and candidates[j].
LevenshteinDistances(match=0, mismatch=1, open=1, extend=1, capabilities=None) computes byte-level edit distances with affine gap penalties.
LevenshteinDistancesUTF8(...) takes the same arguments but counts edits over UTF-8 codepoints instead of raw bytes.
The result matrix is uint64.
| Argument | Default | Meaning |
|---|---|---|
match |
0 |
Cost of a matching character. |
mismatch |
1 |
Cost of a substitution. |
open |
1 |
Cost of opening a gap, meaning an insertion or deletion run. |
extend |
1 |
Cost of extending a gap. |
capabilities |
None |
A capabilities tuple like ('serial', 'parallel'), or a DeviceScope for automatic inference. |
import stringzilla as sz
import stringzillas as szs
engine = szs.LevenshteinDistances()
a = sz.Strs(["hello", "world"])
b = sz.Strs(["hallo", "word"])
distances = engine(a, b) # 2x2 uint64 matrix
assert distances.shape == (2, 2)
assert distances[0, 0] == 1 # hello -> hallo
# UTF-8 codepoint distances
utf8_engine = szs.LevenshteinDistancesUTF8(mismatch=5)
utf8_engine(sz.Strs(["café", "naïve"]), sz.Strs(["caffe", "naive"]))NeedlemanWunschScores(byte_to_class, class_substitution_costs, open=-1, extend=-1, capabilities=None) computes global alignment scores.
SmithWatermanScores(...) takes identical arguments and computes local alignment scores.
Both score from a class-based substitution matrix, and the result matrix is int64.
| Argument | Meaning |
|---|---|
byte_to_class |
A 256-element uint8 NumPy array mapping each byte to one of 32 classes. |
class_substitution_costs |
A 32x32 int8 NumPy matrix of costs between classes. |
open |
Gap-open cost, defaulting to -1. |
extend |
Gap-extend cost, defaulting to -1. |
capabilities |
A capabilities tuple or a DeviceScope. |
import stringzilla as sz
import stringzillas as szs
import numpy as np
classes = (np.arange(256) % 32).astype(np.uint8)
costs = np.eye(32, dtype=np.int8) # reward exact class matches
global_engine = szs.NeedlemanWunschScores(classes, costs)
global_engine(sz.Strs(["ACGT", "TGCA"]), sz.Strs(["ACCT", "TGAA"]))
local_engine = szs.SmithWatermanScores(classes, costs, open=-3, extend=-1)
local_engine(sz.Strs(["ACGTACGT"]), sz.Strs(["CGTACGTA"]))Pass a GPU DeviceScope at construction or per call to run on CUDA, which requires stringzillas-cuda:
gpu = szs.DeviceScope(gpu_device=0)
engine = szs.LevenshteinDistances(match=0, mismatch=2, open=3, extend=1, capabilities=gpu)
distances = engine(a, b, device=gpu)Each engine also exposes a read-only __capabilities__ property reporting the backends it selected at runtime.
The module-level szs.to_device(strs) converts a sz.Strs to use a unified/device-accessible allocator, forcing the allocator swap that normally happens during GPU kernel execution.
Fingerprints(ndim, window_widths=None, alphabet_size=256, seed=0, capabilities=None) computes MinHash-style rolling sketches over many documents in parallel.
The sketches drive near-duplicate detection, clustering, and multi-pattern search, and share the same device model as the alignment engines.
| Argument | Default | Meaning |
|---|---|---|
ndim |
required | Dimensions per fingerprint. |
window_widths |
None |
1-D uint64 array of rolling-window widths. |
alphabet_size |
256 |
Alphabet size. |
seed |
0 |
Reproducibility seed. |
capabilities |
None |
Capabilities tuple or DeviceScope. |
window_widths is a 1-D contiguous uint64 NumPy array; built-in defaults are used when None.
alphabet_size is 256 for arbitrary binary strings.
seed derives independent per-dimension multipliers and moduli for MinHash independence; 0 is just the default seed, not a special mode.
The capabilities tuple may include 'cuda'.
The engine is called as engine(texts, device=None, out=None) and returns a tuple (hashes, counts) of two uint32 NumPy matrices, each of shape (len(texts), ndim).
hashes holds the minimal hash per dimension; counts holds the corresponding match counts.
Fingerprints exposes its selected backends through a read-only capabilities property — note that this name lacks the leading and trailing underscores used by the alignment engines.
import stringzilla as sz
import stringzillas as szs
import numpy as np
engine = szs.Fingerprints(ndim=128)
docs = sz.Strs(["document one", "document two", "document three"])
hashes, counts = engine(docs)
assert hashes.shape == (3, 128)
# Custom window widths and a fixed seed
widths = np.array([4, 8, 16], dtype=np.uint64)
engine = szs.Fingerprints(ndim=256, window_widths=widths, seed=7)
hashes, counts = engine(docs)Str, along with the matching module-level functions, exposes lazy iterators over Unicode boundaries.
Each yields Str views into the original buffer, so segmentation stays allocation-free, except utf8_codepoints, which yields int code points.
| Method / function | Standard | Yields |
|---|---|---|
utf8_codepoints(string) |
scalar values | int code points; ill-formed bytes decode to U+FFFD, so iteration never raises. |
utf8_graphemes(string, skip_empty=False) |
TR29 grapheme clusters | user-perceived characters such as a base plus combining marks, or emoji ZWJ sequences. |
utf8_wordbreaks(string, skip_empty=False) |
TR29 word boundaries | all UAX-29 word segments (words and the separators between them; they tile). |
utf8_sentences(string, skip_empty=False) |
TR29 sentence boundaries | sentences. |
utf8_linebreaks(string, skip_empty=False) |
UAX14 line-break opportunities | soft-wrap segments. |
utf8_split_newlines(string, skip_empty=False, with_separators=False) |
7 Unicode newlines + CRLF | content BETWEEN hard newlines (LF, VT, FF, CR, NEL, U+2028, U+2029, CRLF). |
utf8_newlines(string, skip_empty=False) |
7 Unicode newlines + CRLF | the newline runs themselves (the separators). |
utf8_split_whitespaces(string, skip_empty=False, with_separators=False) |
25 Unicode "White_Space" |
content BETWEEN whitespace runs, like str.split() with no separator. |
utf8_whitespaces(string, skip_empty=False) |
25 Unicode "White_Space" chars |
the whitespace runs themselves (the separators). |
utf8_split_delimiters(string, skip_empty=False, with_separators=False) |
punctuation/symbol/separator | content BETWEEN any Unicode delimiter (superset of whitespace). |
utf8_delimiters(string, skip_empty=False) |
punctuation/symbol/separator | the delimiter runs themselves (the separators). |
Naming follows one rule: the bare name (newlines/whitespaces/delimiters) yields the separators, while split_* yields the content between them.
skip_empty drops empty segments; with_separators=True interleaves both losslessly (concatenation reproduces the input), replacing the old keepends.
import stringzilla as sz
assert list(sz.utf8_codepoints("AB")) == [65, 66]
assert [str(g) for g in sz.Str("a👍🏽b").utf8_graphemes()] == ["a", "👍🏽", "b"]
[str(w) for w in sz.utf8_wordbreaks("Hi, world")] # all UAX-29 segments
assert sum(1 for _ in sz.Str("first\nsecond\nthird").utf8_split_newlines()) == 3
assert [str(t) for t in sz.utf8_split_whitespaces("foo bar baz", skip_empty=True)] == ["foo", "bar", "baz"]
assert "".join(str(s) for s in sz.utf8_split_newlines("a\nb", with_separators=True)) == "a\nb"utf8_count(string) counts Unicode characters rather than bytes, unlike len, which counts bytes:
import stringzilla as sz
assert sz.utf8_count("hello") == 5
assert sz.utf8_count("é") == 1
assert len(sz.Str("é")) == 2 # bytesThese apply Unicode case folding, correctly handling one-to-many expansions such as German ß matching SS.
utf8_uncased_fold(text, validate=False)— return the case-folded UTF-8 string asbytes.utf8_uncased_search(haystack, needle, start=0, end=len, validate=False)— index of the first uncased match, or-1. Forstrinputsstart/endand the result are codepoint offsets; forbytesinputs they are byte offsets.utf8_uncased_order(a, b, validate=False)— uncased lexicographic comparison: negative, zero, or positiveint.utf8_uncased_matches(haystack, needle, include_overlapping=False)— iterate over all uncased matches, yielding each matched region as aStrview whose length may differ fromneedledue to folding expansions.
Pass validate=True to validate UTF-8 before processing.
import stringzilla as sz
assert sz.utf8_uncased_fold("HELLO") == b"hello"
assert sz.utf8_uncased_fold("Straße") == b"strasse"
assert sz.utf8_uncased_search("Hello World", "WORLD") == 6
assert sz.utf8_uncased_order("hello", "HELLO") == 0
assert [str(m) for m in sz.utf8_uncased_matches("Hello HELLO hello", "hello")] == ["Hello", "HELLO", "hello"]These mirror unicodedata.normalize but operate on raw UTF-8 bytes.
utf8_norm(text, form, validate=False)— normalize to one of'NFC','NFD','NFKC','NFKD'; returnsbytes.utf8_find_denormalized(text, form)— return the byte offset of the first codepoint that breaks the given normalization form, orNoneif the string is already fully normalized.
import stringzilla as sz
assert sz.utf8_norm("café", "NFD") == b"cafe\xcc\x81" # decomposed
assert sz.utf8_norm("fi", "NFKD") == b"fi" # ligature expanded
assert sz.utf8_find_denormalized("café", "NFC") is None # already NFCStringZilla detects the running CPU's SIMD features at import time and routes every kernel to the fastest available backend without recompilation.
sz.__version__— the package version string.sz.__capabilities__— a tuple of the detected backends, e.g.('serial', 'haswell', 'skylake', 'ice').sz.reset_capabilities(names)— restrict the active backends tonames, intersected with the hardware's actual capabilities; if the intersection is empty it falls back to'serial'. This updatessz.__capabilities__and re-points the dispatch table, which is useful for testing, benchmarking one backend, or reproducibility.
import stringzilla as sz
sz.__capabilities__ # e.g. ('serial', 'haswell', 'skylake', 'ice')
sz.reset_capabilities(["serial"]) # force the scalar backend for this moduleThe stringzillas module has the same controls.
szs.reset_capabilities(names) sets its default backends, intersected with hardware and falling back to 'serial', and each batch engine reports the backends it selected through a read-only property — __capabilities__ on the four alignment engines and capabilities on Fingerprints.
import stringzillas as szs
szs.reset_capabilities(("serial",)) # restrict batch dispatch to the scalar backend
szs.LevenshteinDistances().__capabilities__ # backends this engine will use