Pandas interoperability through the shared Python bridge
NumSharp.Interop.pythonnet includes a built-in PandasPythonArrayAdapter for Pandas
DataFrame, Series, Index, ExtensionArray, and derived types. It does not implement another
pointer bridge. The adapter selects Pandas' official to_numpy
projection; NDArrayPythonInterop still owns dtype, shape, strides, writeability, copying, leases,
GIL policy, and shutdown behavior.
Verified live on CPython 3.12.12 · NumPy 2.4.2 · pythonnet 3.0.5 · Pandas 3.0.5 · net8.0/net10.0. The claims on this page are exercised against the installed package by 29 tests in
PandasInteropTests,PandasInteropEdgeCaseTests, andPandasInteropRareScenarioTests.
Setup
Install Pandas into the interpreter embedded by pythonnet:
python -m pip install pandas==3.0.5
Runtime.PythonDLL = "python312.dll"; // or PYTHONNET_PYDLL
PythonEngine.Initialize();
using (Py.GIL())
{
dynamic pd = Py.Import("pandas");
}
There is no compile-time Pandas dependency and no Pandas .NET wrapper dependency.
Pandas to NumSharp
Use the ordinary bridge verbs directly on the Pandas PyObject:
using (Py.GIL())
{
using PyObject series = scope.Eval("pd.Series([1, 2, 3], dtype='i8')");
using NDArray view = series.AsNDArray(allowReadonly: true);
using NDArray copy = series.ToNDArray();
}
| Verb | Contract |
|---|---|
pandasObject.AsNDArray(allowReadonly: true) |
zero-copy only when stable shared storage is proven; otherwise throws |
pandasObject.ToNDArray() |
independent C-contiguous snapshot; Pandas may coerce/materialize first |
pandasObject.As<NDArray>() after RegisterCodec() |
Auto: verified view first, then owning copy |
Pandas 3 commonly marks a shared NumPy projection read-only under its mandatory
Copy-on-Write rules. Pass
allowReadonly:true; NumSharp returns Shape.IsWriteable == false, and guarded writes fail instead
of bypassing Pandas' policy. Calling bare AsNDArray() intentionally rejects that read-only source.
Why copy:false is not enough
Pandas documents that to_numpy(copy:false) does not guarantee no copy. A homogeneous frame can
be one NumPy-backed block, while mixed columns need a common dtype and a materialized array. Nullable,
categorical, sparse, and other extension arrays also decide individually.
For View mode the adapter requests two independent official projections and requires
numpy.shares_memory(first, second). If they do not overlap, no stable Pandas-owned projection can
be proven and View declines with guidance to use Copy or Auto. Empty arrays are accepted because
they have no addressable element whose sharing could be misrepresented. The selected projection
then enters the same locked-buffer / array-interface bridge used by NumPy and Torch.
Pandas object
→ PandasPythonArrayAdapter.to_numpy()
→ stable-overlap check (View only)
→ NDArrayPythonInterop.ToNDArrayView / ToNDArray
→ existing dtype + layout + lease machinery
NumSharp to Pandas
Pandas already accepts NumPy, so the existing NumSharp encoder is the correct route:
NDArrayPythonInterop.RegisterCodec();
using (Py.GIL())
{
scope.Set("values", nd); // implicit NDArray → NumPy view
scope.Exec("series = pd.Series(values, copy=False)");
scope.Exec("frame = pd.DataFrame(values.reshape(-1, 2), copy=False)");
}
copy=False is important when sharing is intended. Pandas 3 constructors otherwise copy NumPy
inputs by default. An explicit copy=False over an external NumSharp-backed NumPy array shares the
storage: NumSharp writes are visible to Pandas, and Pandas writes are visible to NumSharp. Pandas'
Copy-on-Write tracking cannot detach an owner outside Pandas, so this is a deliberate shared-memory
opt-in, not ordinary Pandas-to-Pandas CoW isolation.
What shares and what copies
| Pandas source | View | Copy / Auto fallback |
|---|---|---|
homogeneous numeric DataFrame |
yes; often F-strided and read-only | detached same dtype |
NumPy-backed numeric Series |
yes; slices and negative strides preserved | detached |
numeric Index / RangeIndex |
yes; read-only | detached |
| nullable integer without missing values | yes when Pandas exposes its stable numeric buffer | detached |
| mixed numeric frame | no; common dtype requires materialization | yes, using Pandas' upcast |
nullable integer with pd.NA |
no | typically float64 with NaN |
| numeric categorical / sparse | no | categorical values / dense numeric values |
| complex64 | no NumSharp view | widens to NumSharp complex128 on copy |
object, string, datetime, timedelta, timezone, MultiIndex values |
unsupported | unsupported: no corresponding NumSharp dtype |
Axis labels, names, duplicate columns, and hierarchical row labels are Pandas metadata. The adapter
returns only the two-dimensional value matrix, exactly like DataFrame.to_numpy().
Codec policy
NDArrayPythonInterop.RegisterCodec(new NumpyCodecOptions
{
DecodeMode = NumpyCodecMode.Auto,
DecodeArrayAdapters = true,
});
Viewaccepts only a proven stable projection.Copyalways returns an owning NumSharp snapshot when the projected dtype is representable.Autotries View and falls back to Copy.DecodeArrayAdapters = falseexcludes Pandas/Torch adapters without disabling native NumPy or buffer sources.
The adapter is registered automatically.
PythonArrayAdapterRegistry.Register(PandasPythonArrayAdapter.Instance) therefore returns false
because the name already exists.
Edge behavior pinned by tests
- scalar, empty Series, zero-row frames, zero-column frames, empty RangeIndex, and 1×1 frames;
- C/F layouts, positive strides, reversed negative strides, and storage offsets;
- all 15 NumSharp source dtypes, including Char→uint16 and Decimal→float64 boundaries;
- NaN payloads, infinities, signed zero, unsigned maxima, and complex values;
- mixed blocks, nullable missing values, categorical, sparse, object and semantic time dtypes;
- duplicate labels and MultiIndex axes;
- Pandas subclasses and public extension-array base classes;
- derived NumSharp-view lifetime after every Pandas/Python wrapper is deleted;
- explicit caller-owned GIL mode, four-thread churn, disposed wrappers, failing overrides, and leak counters after every unsuccessful conversion;
- full NumSharp→Pandas→NumSharp pointer/shape/stride identity for positive, negative, offset, Fortran, and broadcast layouts;
- every nullable integer/unsigned/float/boolean extension dtype, both mask-free and with
pd.NA; - float16/float32, int64/uint64, real/complex, and object-producing column-promotion boundaries;
- big-endian integer/float/complex view rejection plus component-wise byte swapping on copy;
- imported-view resize refusal, owning-copy resize, NumSharp-export lifetime under Pandas ownership,
malformed
to_numpyreturns, descending RangeIndex, and two-axis negative strides.
Claims ledger
| # | Claim | Gate |
|---|---|---|
| 1 | validated runtime is Pandas 3.0.5 | Runtime_IsLatestStablePandas305 |
| 2 | built-in recognition covers frames, series, indexes, extension arrays and subclasses—not scalars | BuiltInAdapter_RecognizesFrameSeriesIndexExtensionAndSubclasses_NotScalars |
| 3 | implicit NumSharp encoding plus explicit Pandas copy=False shares storage |
NumSharpToPandas_ImplicitEncoderSharesInput_WhenCopyFalseIsExplicit |
| 4 | numeric Series enters the existing read-only view bridge with exact pointer/layout | NumericSeries_UsesExistingReadonlyViewBridge_WithExactPointerAndLayout |
| 5 | homogeneous frame preserves its F-strided matrix and pointer through implicit decode | HomogeneousDataFrame_ImplicitDecoderPreservesFortranStridesAndPointer |
| 6 | numeric Index and nullable-without-missing extension storage are stable views | IndexAndNullableExtensionWithoutMissingValues_AreStableViews |
| 7 | mixed numeric frame copies, detaches, and follows Pandas' common-dtype upcast | MixedNumericFrame_OrdinaryCopyAndImplicitAutoAreDetachedAndUpcastByPandas |
| 8 | materializing numeric families reject View but succeed through Copy | MaterializingPandasObjects_RejectView_ButNumericCopiesPreservePandasValues |
| 9 | object/string/time/nested-index values fail without leases | ObjectStringDatetimeTimedeltaAndNestedIndex_AreDirectedUnsupportedFailures |
| 10 | complex64 copy-widens while View declines | Complex64ViewDeclines_AndCopyWidensToNumSharpComplex128 |
| 11 | empty and degenerate ranks/shapes/dtypes are preserved | EmptyAndDegenerateShapes_PreserveRankShapeAndResolvedDtype |
| 12 | positive and negative Series strides preserve logical values | PositiveAndNegativeStridedSeries_PreserveLogicalOffsetsAndStrides |
| 13 | all 15 NumSharp source dtypes round-trip through a numeric Series | All15NumSharpDtypes_RoundTripThroughPandasNumericSeries |
| 14 | labels are metadata; duplicate/MultiIndex axes do not alter value order | AxisLabelsDuplicatesAndMultiIndex_AreMetadataOnly_ValuesKeepTwoDimensionalOrder |
| 15 | derived NumSharp views retain Pandas storage after Python wrappers die | DerivedNumSharpView_KeepsPandasStorageAliveAfterAllPythonWrappersDie |
| 16 | View/Copy/Auto and adapter-disable policies remain distinct | CodecModes_RespectPandasViewCopyAutoAndExplicitAdapterDisable |
| 17 | caller-owned GIL mode works for view and copy | ExplicitNoGilPolicy_WorksInsideOneOuterGil_ForPandasViewAndCopy |
| 18 | concurrent round-trips drain every export/import counter | ConcurrentPandasRoundTrips_AreThreadSafeAndDrainEveryLifetimeCounter |
| 19 | disposed and broken Pandas objects fail cleanly without leases | DisposedAndBrokenPandasObjects_FailCleanlyWithoutLeasingMemory |
| 20 | NumSharp layouts round-trip through Pandas with identical pointer, shape and strides | NumSharpLayouts_RoundTripThroughPandasWithoutLosingPointerShapeOrStrides |
| 21 | every mask-free nullable extension dtype exposes its resolved stable typed buffer | NullableExtensionDtypesWithoutMissingValues_ExposeEveryStableTypedBuffer |
| 22 | nullable extension masks reject View and resolve Copy dtype/NaN precisely | NullableExtensionDtypesWithMissingValues_RejectViewAndResolveCopyDtypePrecisely |
| 23 | mixed-column promotion follows Pandas, including uint64→float64 precision boundaries | MixedColumnPromotion_FollowsPandasCommonDtypeIncludingPrecisionBoundaries |
| 24 | big-endian integer, float and complex values reject View and byte-swap correctly on Copy | BigEndianNumericSeries_RejectView_AndCopyByteSwapsEveryScalarComponent |
| 25 | imported views cannot resize/detach while owning copies can | ImportedPandasViewCannotResizeOrDetach_WhileOwningCopyCan |
| 26 | Pandas retains a NumSharp export after the original NDArray is disposed | PandasObject_KeepsNumSharpExportAliveAfterOriginalArrayIsDisposed |
| 27 | Pandas mutations update a leased view while an owning copy stays detached | PandasMutationUpdatesLeasedView_ButOwningCopyRemainsDetached |
| 28 | malformed to_numpy return types fail without acquiring import leases |
MalformedToNumpyReturnTypes_FailWithoutAcquiringImportLeases |
| 29 | descending RangeIndex and two-axis negative-stride frames preserve logical order | DescendingRangeIndexAndTwoAxisReorderedFrame_PreserveUnusualLogicalOrder |