Repository navigation
[Core] NumPy String support in NumSharp #592
Copy link
Copy link
Open
Labels
NumPy 2.x ComplianceAligns behavior with NumPy 2.x (NEPs, breaking changes)Aligns behavior with NumPy 2.x (NEPs, breaking changes)apiPublic API surface (np.*, NDArray methods, operators)Public API surface (np.*, NDArray methods, operators)coreInternal engine: Shape, Storage, TensorEngine, iteratorsInternal engine: Shape, Storage, TensorEngine, iteratorsenhancementNew feature or requestNew feature or requestmissing feature/sNumPy function not yet implemented in NumSharpNumPy function not yet implemented in NumSharp
Milestone
Description
Overview
Add NumPy-compatible string dtype support to NumSharp, enabling text data processing with full NumPy 2.x behavioral parity.
Problem
NumSharp cannot store strings in arrays, perform vectorized string operations, or interoperate with NumPy string data. This blocks text-heavy ML/data science workflows, pandas-like DataFrames, and full .npy file compatibility.
Reference
docs/plans/NUMPY_STRING_TYPES.md - 1959-line specification from NumPy v2.4.2 source analysis.
Proposal
Core Type System
- Add
NPTypeCode.Bytesfor fixed-width byte strings (S, NPY_STRING = 18) - Add
NPTypeCode.Unicodefor fixed-width Unicode strings (U, NPY_UNICODE = 19) - Add
NPTypeCode.VStringfor variable-length UTF-8 strings (T, NPY_VSTRING = 2056) - Keep
NPTypeCode.Charas its own type (maps toU1orS1internally for string operations) - Add
np.bytes_scalar type (wrapsbyte[]) - Add
np.str_scalar type (wrapsstringorint[]for UTF-32) - Add
np.characterbase class for string scalar hierarchy - Add
np.flexiblebase class (parent ofcharacterandvoid)
Dtype Parsing and Construction
- Parse
S,S5,S10,|S10to bytes_ dtype - Parse
U,U5,U10to str_ dtype - Parse
<U10(little-endian),>U10(big-endian),=U10(native) byte orders - Parse
Tto StringDType - Parse
aas deprecated alias forS - Parse
cdtype - map toNPTypeCode.Charor treat asS1 - Support
np.dtype(np.bytes_),np.dtype(np.str_)constructor forms - Support tuple form:
np.dtype((np.str_, 5))for 5-char unicode - Implement
dtype.charproperty (S,U,T) - Implement
dtype.kindproperty (S,U,T) - Implement
dtype.typeproperty (scalar type class) - Implement
dtype.itemsize(bytes per element) - Implement
dtype.name(e.g.,bytes80,str320,StringDType128) - Implement
dtype.str(e.g.,|S10,<U10,|T16)
Memory Layout - bytes_ (S)
- Store as contiguous byte buffer, 1 byte per character
- Itemsize = exact byte count per element
- Null-pad shorter strings to itemsize
- Truncate longer strings to itemsize
- No byte order (always
|prefix) - Strip trailing nulls on scalar access
Memory Layout - str_ (U)
- Store as UTF-32/UCS-4, 4 bytes per code point (
npy_ucs4=uint) - Itemsize = character count x 4
- Support little-endian (
<) and big-endian (>) byte orders - Null-pad shorter strings (null =
0x00000000) - Truncate longer strings to character count
- Implement byte swapping for cross-platform .npy files
Memory Layout - StringDType (T)
- 16-byte header per element (64-bit) or 8-byte (32-bit)
- Implement small-string optimization (15 bytes or less inline on 64-bit, 7 or less on 32-bit)
- Implement arena allocator for medium strings (255 bytes or less)
- Implement heap allocation for large/mutated strings
- Store UTF-8 encoded content
- Implement flag byte:
MISSING(0x80),INITIALIZED(0x40),OUTSIDE_ARENA(0x20),LONG(0x10) - Thread-safe allocator access (mutex/lock)
- Preserve embedded null bytes (unlike legacy types)
StringDType NA/Missing Support
- Support
StringDType(na_object=None)- None as NA - Support
StringDType(na_object=np.nan)- NaN as NA - Support
StringDType(coerce=True/False)- coercion control - NA values sort to end
-
np.isnan()identifies NaN-like NA - Raise on comparison with non-NaN NA (e.g., None)
Array Creation
-
np.array(["hello", "world"], dtype="S")- auto-size to longest -
np.array(["hello", "world"], dtype="U")- auto-size to longest -
np.array(["hello", "world"], dtype="T")- variable length -
np.array([b"hello"], dtype="S")- from Python bytes -
np.array(["hello"], dtype="S")- encode string as ASCII - Truncation when explicit size < content:
dtype="U3"results in"hel" - Null-padding when explicit size > content
-
np.zeros((3,), dtype="U10")- empty strings -
np.empty((3,), dtype="U10")- uninitialized (may contain garbage) -
np.full((3,), "x", dtype="U10")- fill with string
Type Conversions and Casting
-
bytes_.astype("U")- decode as ASCII to Unicode -
str_.astype("S")- encode as ASCII (fail on non-ASCII) -
str_.astype("T")- full Unicode preserved -
StringDType.astype("U10")- may truncate to width -
StringDType.astype("S")- ASCII only - Width truncation:
"U10".astype("U5")truncates -
int/float.astype("U")results in["1", "1.5"] -
["1", "2"].astype(int)results in[1, 2] -
bool.astype("U")results in["True", "False"] -
string.astype(bool)- empty = False, non-empty = True -
datetime64.astype("T")results in ISO string -
StringDType.astype("datetime64")- parse ISO,"NaT"becomes NaT -
StringDType.astype("V5")- raw UTF-8 bytes to void - Casting safety:
casting="safe"vs"unsafe" -
NPTypeCode.Charconversion to/from string types
Comparison Operations
-
==element-wise equality (returns bool array) -
!=element-wise inequality -
<,<=,>,>=lexicographic comparison -
np.equal,np.not_equal,np.less,np.less_equal,np.greater,np.greater_equal - Comparison broadcasts correctly
- bytes_ and str_ cannot be compared directly (TypeError)
- StringDType can compare with str_ and object (type promotion)
String Concatenation and Repetition
-
np.strings.add(a, b)- concatenate strings -
np.strings.multiply(a, n)- repeat string n times -
multiply(s, 0)ormultiply(s, -1)results in empty string -
multiply(s, sys.maxsize)results in OverflowError - Broadcasting:
["a", "b"] + "x"results in["ax", "bx"]
String Length
-
np.strings.str_len(arr)- returns int64 array - Length = code points, not bytes (emoji = 1)
- Empty string results in 0
String Searching
-
np.strings.find(a, sub, start=0, end=None)- first occurrence, -1 if not found -
np.strings.rfind(a, sub, start=0, end=None)- last occurrence -
np.strings.index(a, sub, ...)- like find but raises ValueError -
np.strings.rindex(a, sub, ...)- like rfind but raises ValueError -
np.strings.count(a, sub, start=0, end=None)- count occurrences -
np.strings.startswith(a, prefix, start=0, end=None)- bool array -
np.strings.endswith(a, suffix, start=0, end=None)- bool array
Case Operations
-
np.strings.upper(arr)- uppercase -
np.strings.lower(arr)- lowercase -
np.strings.capitalize(arr)- first char upper, rest lower -
np.strings.title(arr)- titlecase each word -
np.strings.swapcase(arr)- swap case
Character Classification
-
np.strings.isalpha(arr)- all alphabetic -
np.strings.isdigit(arr)- all digits -
np.strings.isalnum(arr)- all alphanumeric -
np.strings.isspace(arr)- all whitespace -
np.strings.islower(arr)- all lowercase (cased chars) -
np.strings.isupper(arr)- all uppercase (cased chars) -
np.strings.istitle(arr)- titlecase format -
np.strings.isnumeric(arr)- numeric (Unicode-aware, str_/StringDType only) -
np.strings.isdecimal(arr)- decimal digits (Unicode-aware, str_/StringDType only) - Empty strings return False for all classification functions
Whitespace Operations
-
np.strings.strip(arr, chars=None)- strip both ends -
np.strings.lstrip(arr, chars=None)- strip left -
np.strings.rstrip(arr, chars=None)- strip right -
np.strings.center(arr, width, fillchar=" ")- center-align -
np.strings.ljust(arr, width, fillchar=" ")- left-align -
np.strings.rjust(arr, width, fillchar=" ")- right-align -
np.strings.zfill(arr, width)- zero-fill numeric strings -
np.strings.expandtabs(arr, tabsize=8)- expand tabs
Splitting and Partitioning
-
np.strings.partition(arr, sep)- returns tuple of 3 arrays (before, sep, after) -
np.strings.rpartition(arr, sep)- partition from right -
np.char.split(arr, sep, maxsplit)- returns object array of lists -
np.char.rsplit(arr, sep, maxsplit)- split from right -
np.char.splitlines(arr, keepends=False)- split on line boundaries -
np.char.join(sep, seq)- join sequences
Replacement and Slicing
-
np.strings.replace(arr, old, new, count=-1)- replace occurrences -
np.strings.slice(arr, start=0, stop=None, step=1)- substring extraction -
np.strings.slice(arr, None, None, -1)- reverse string
Encoding/Decoding
-
np.strings.encode(arr, encoding="utf-8", errors="strict")- str to bytes -
np.strings.decode(arr, encoding="utf-8", errors="strict")- bytes to str - Error modes:
"strict","ignore","replace"
Formatting and Translation
-
np.strings.mod(format_arr, values)- printf-style formatting -
np.strings.translate(arr, table, deletechars=None)- character translation
Array Functions with String Support
-
np.sort(arr)- lexicographic sort -
np.argsort(arr)- sort indices -
np.argmax(arr)- index of lexicographic max -
np.argmin(arr)- index of lexicographic min -
np.searchsorted(arr, value)- binary search -
np.unique(arr)- unique strings -
np.concatenate([arr1, arr2])- join arrays -
np.stack,np.vstack,np.hstack,np.dstack- stacking -
np.where(cond, x, y)- conditional selection -
np.take(arr, indices)- index selection -
np.nonzero(arr)- indices of non-empty strings -
np.any(arr)- True if any non-empty -
np.all(arr)- True if all non-empty -
np.copy(arr)- copy array -
np.resize(arr, new_shape)- resize with repetition -
np.maximum(a, b),np.minimum(a, b)- lexicographic element-wise
Structured Arrays
- String fields in structured dtypes:
[("name", "U20"), ("id", "i4")] - StringDType in structured arrays
- Nested structured types with string fields
- Record arrays (
np.rec.array) with attribute access
File I/O
-
np.save()/np.load()for bytes_, str_, StringDType (.npy format) -
np.savez()/np.load()for string arrays (.npz archives) -
np.loadtxt(dtype="U")- load text as strings -
np.savetxt(fmt="%s")- save strings to text -
np.genfromtxt(dtype="U")- flexible text loading -
arr.tofile()/np.fromfile(dtype="S10")- binary I/O (fixed-width only) - Memory mapping for fixed-width strings:
np.memmap(dtype="U100") - StringDType cannot be memory-mapped (variable length)
Boolean Conversion and Truthiness
-
arr.astype(bool)- empty = False, non-empty = True -
np.nonzero(arr)returns indices of non-empty strings - StringDType NA is truthy for nonzero (but identifiable via
np.isnan)
Edge Cases
- Empty string arrays:
np.array([""], dtype="S")results indtype="|S1" - Zero-size dtype:
np.dtype("S0").itemsizeresults in 0 - Embedded null bytes preserved in storage
- Null bytes may terminate on scalar access (bytes_)
- Unicode surrogates: valid in UTF-32, may fail converting to UTF-8
- Combining characters:
"e\u0301"is 2 code points (displays as 1) - No automatic Unicode normalization
- Very large strings: respect
NPY_MAX_STRING_SIZE - Overflow detection in
multiplyandadd
Legacy Support
-
np.char.chararray- deprecated subclass (minimal support if needed) - chararray automatic whitespace stripping behavior
- chararray instance methods (upper, lower, etc.)
-
np.char.*functions as aliases tonp.strings.*
Type Checking
-
np.issubdtype(arr.dtype, np.bytes_)- True for S -
np.issubdtype(arr.dtype, np.str_)- True for U -
np.issubdtype(arr.dtype, np.character)- True for S or U -
np.issubdtype(arr.dtype, np.flexible)- True for S, U, V
Evidence
- Reference document:
docs/plans/NUMPY_STRING_TYPES.md - NumPy source:
src/numpy/(v2.4.2) - String dtypes required for: pandas interop, scikit-learn text vectorizers, NLP pipelines, CSV/text file processing
Scope / Non-goals
In scope: Core string dtypes, string operations API, file I/O, structured arrays, type conversions
Out of scope:
- Regular expressions (use .NET
Regex) - Locale-aware collation/sorting
- NLP-specific operations (tokenization, stemming)
- Full chararray compatibility (deprecated in NumPy)
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
NumPy 2.x ComplianceAligns behavior with NumPy 2.x (NEPs, breaking changes)Aligns behavior with NumPy 2.x (NEPs, breaking changes)apiPublic API surface (np.*, NDArray methods, operators)Public API surface (np.*, NDArray methods, operators)coreInternal engine: Shape, Storage, TensorEngine, iteratorsInternal engine: Shape, Storage, TensorEngine, iteratorsenhancementNew feature or requestNew feature or requestmissing feature/sNumPy function not yet implemented in NumSharpNumPy function not yet implemented in NumSharp
Activity
Nucs commentedon Mar 18, 2026
Current String Handling in NumSharp
NPTypeCode Enum
NPTypeCode.CharcharNPTypeCode.StringstringKey Files
Backends/NPTypeCode.csBackends/NDArray.String.csCreation/np.array.string.csnp.array(string[])implementationCreation/np.asarray.csnp.asarray(string)implementationCreation/np.dtype.cs'S','U'->char)APIs/np.save.csCurrent Capabilities
1. Single String -> Char Array
2. Char Array Operations
3. String Array -> Encoded Format (Hack)
4. np.save/np.load String Support
Dtype Parsing
NPY_TYPECHAR Mapping
Limitations
NPTypeCode.Stringexists but is barely usable - most operations throw or fall back to chardtype='U10'with 10 characters per elementchar(UTF-16), not NumPy's UTF-32'S'dtype doesn't store bytes, just charsnp.array(string[])uses a metadata format, not real string storage'S10','U10'parsing ignores the size specifiernp.savewrites strings as single-byte ASCIInp.strings.*ornp.char.*functionsType Priority / Group
Size Reporting (Bug?)
This is a far cry from NumPy's string support. The current implementation is essentially "store chars and provide helper methods to convert to/from strings."
Nucs commentedon Jun 20, 2026
Current-state compatibility matrix (NumPy 2.4.2 vs NumSharp now)
Mapping of every string capability against the current implementation, to scope this issue. Verified live against NumPy 2.4.2 and ground-truthed in source (
nditer@f7be5f7e). Saved in-repo atdocs/numpy/STRING_COMPAT_MATRIX.md, next to the spec.Legend: ✅ present & compatible · 🟡 partial / hack / divergent · ❌ missing or throws
A. Type system / dtypes
bytes_'S'(enum 18)NPTypeCode.Bytes;dtype('S')throwsNotSupportedExceptionstr_'U'(enum 19)NPTypeCode.Unicode;dtype('U')throwsStringDType'T'(enum 2056)dtype('T')throws ("cannot parse")'c'char dtypeS1(1 byte)dtype('c')throws (explicitly in_unsupported_numpy_codes)NPTypeCode.Charchar(UTF-16, 2 B, reported size 1)NPTypeCode.String(18)typeof(string),SizeOf=1, barely wiredobjectdtype holding stringsB. Dtype parsing & properties
dtype('S'/'S10'/'|S10')dtype('U'/'U10'/'<U10'/'>U10')dtype('T'),dtype('a')dtype('string')/dtype('char')typeof(string)/typeof(char)(NumSharp-only aliases)dtype((np.str_,5)).char/.kind=S/U/TDType.kindhardcodes'S'for Char & String; wrong for real strings.itemsize(exact bytes)S10→10,U10→40,T→16.name(bytes80/str320/StringDType128)"string"only.str(|S10/<U10/|T16).type(scalar class)C. Scalars & type hierarchy
np.bytes_/np.str_scalar typesnp.character/np.flexiblebasesissubdtype(d, np.character/flexible/bytes_/str_)D. Array creation
np.array([...], dtype='S'/'U'/'T')np.array(string[])<Uarray"N l1 l2:contents"b'...'bytesnp.zeros/empty/full(dtype='U10')new NDArray(typeof(string), shape)NotSupportedException(issue #341)string→ char vector (FromString, implicit op)(n,)char)GetString/SetString/GetStringAton char vectorsE. Memory layout
F. Casting / conversion
'1','1.5','nan','inf')<U21for int64)'22'→22)'True'/'False')'False'→True)S↔U↔T, width truncation, casting safetydatetime64 ↔ strG. Comparisons
==/!=element-wise on strings<,<=,>,>=lexicographicSvsUmismatch (==→all-False,<→raises)H. String operations —
np.strings(46 ufuncs) &np.charadd(concat),multiply(repeat)str_len(code points)find/rfind/index/rindex/count/startswith/endswithupper/lower/capitalize/title/swapcaseisalpha/isdigit/isalnum/isspace/islower/isupper/istitle/isnumeric/isdecimalstrip/lstrip/rstrip/center/ljust/rjust/zfill/expandtabsreplace,slicepartition/rpartitionsplit/rsplit/splitlines/join(object arrays)encode/decode,mod,translatenp.char.*legacy +chararrayI. Array functions with string support
sort/argsort(lexicographic)np.sortmissing entirely;argsortnumeric-onlyargmax/argmin(lexicographic)searchsorted,uniqueconcatenate/stack/vstack/hstack/dstackwhere/take/nonzero/any/all(empty=False)J. File I/O & structured arrays
np.savestring array →.npy|S/<U/|Tpreservedstring[]only →|S{maxlen}, ASCII-lossy(byte)s[i], noU/Tnp.load.npystring|S→ managedstring[](null-terminated ASCII); noU/T; not a string-dtype NDArraysavez/npz,loadtxt/savetxt/genfromtxt,tofile/fromfile,memmaprecarrayTally (≈70 capabilities)
string[]packing hack, ASCII.npyround-trip, fake dtype propsnp.strings/np.charop, casting, comparisons, structured/text I/OBottom line
NumSharp today has no real string dtype — only a
char(UTF-16) vector dressed up with helper methods, one brittlestring[]→charpacking hack, and an ASCII-only.npystring[]path that bypasses the NDArray dtype system entirely. The two "🟡 file I/O" rows are the only places a multi-string array survives a round trip, and only for ASCII.Generated from NumSharp
nditer@f7be5f7eand NumPy v2.4.2 live verification.Add NumPy-NumSharp string compatibility matrix
Add NumPy-NumSharp string compatibility matrix