Repository: duckdb/duckdb (language: python)

Issue:
Can not cast field type to Int64 when querying nullable Int64 fields from a Pandas.DataFrame
### What happens?

As indicated in the title and attachment, even when using the cast function, it is impossible to maintain the cc_id in the dataframe returned by duckdb.query as Int64; the cc_id has been converted to a float64 type.
![image](https://github.com/user-attachments/assets/70ca1382-f7c2-4c7b-9510-c436e947ed3d)


### To Reproduce

```
import pandas as pd
import duckdb

df = pd.DataFrame({
    'cc_id': pd.Series([1844743171901218817, 1844691776961568771, None, None], dtype='Int64'),
    'other_column': [10, 20, 30, 40]
})

result = duckdb.query("SELECT CAST(cc_id AS Int64) AS cc_id, other_column FROM df WHERE other_column > 10").df()
result['cc_id']
```

### OS:

macOS 12.7.6

### DuckDB Version:

0.10.2, 1.1.1

### DuckDB Client:

Python

### Hardware:

_No response_

### Full Name:

Kafka Liu

### Affiliation:

Freelancer

### What is the latest build you tested with? If possible, we recommend testing with the latest nightly build.

I have tested with a stable release

### Did you include all relevant data sets for reproducing the issue?

Yes

### Did you include all code required to reproduce the issue?

- [x] Yes, I have

### Did you include all relevant configuration (e.g., CPU architecture, Python version, Linux distribution) to reproduce the issue?

- [x] Yes, I have


Discussion / hints:

Fix patch:
diff --git a/src/include/duckdb/common/types/validity_mask.hpp b/src/include/duckdb/common/types/validity_mask.hpp
index 897488e5fdbf..880bd9ff8410 100644
--- a/src/include/duckdb/common/types/validity_mask.hpp
+++ b/src/include/duckdb/common/types/validity_mask.hpp
@@ -155,6 +155,7 @@ struct TemplatedValidityMask {
 		return validity_mask[entry_idx];
 	}
 	static inline bool AllValid(V entry) {
+		// Check if all the tuples that are covered by this entry (usually 64) are valid
 		return entry == ValidityBuffer::MAX_ENTRY;
 	}
 	static inline bool NoneValid(V entry) {
diff --git a/tools/pythonpkg/scripts/cache_data.json b/tools/pythonpkg/scripts/cache_data.json
index 7aa110e3b074..44408d142a75 100644
--- a/tools/pythonpkg/scripts/cache_data.json
+++ b/tools/pythonpkg/scripts/cache_data.json
@@ -51,7 +51,18 @@
             "pandas.isnull",
             "pandas.ArrowDtype",
             "pandas.NaT",
-            "pandas.NA"
+            "pandas.NA",
+            "pandas.BooleanDtype",
+            "pandas.UInt8Dtype",
+            "pandas.UInt16Dtype",
+            "pandas.UInt32Dtype",
+            "pandas.UInt64Dtype",
+            "pandas.Int8Dtype",
+            "pandas.Int16Dtype",
+            "pandas.Int32Dtype",
+            "pandas.Int64Dtype",
+            "pandas.Float32Dtype",
+            "pandas.Float64Dtype"
         ],
         "required": false
     },
@@ -85,6 +96,72 @@
         "name": "ArrowDtype",
         "children": []
     },
+    "pandas.BooleanDtype": {
+        "type": "attribute",
+        "full_path": "pandas.BooleanDtype",
+        "name": "BooleanDtype",
+        "children": []
+    },
+    "pandas.UInt8Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.UInt8Dtype",
+        "name": "UInt8Dtype",
+        "children": []
+    },
+    "pandas.UInt16Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.UInt16Dtype",
+        "name": "UInt16Dtype",
+        "children": []
+    },
+    "pandas.UInt32Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.UInt32Dtype",
+        "name": "UInt32Dtype",
+        "children": []
+    },
+    "pandas.UInt64Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.UInt64Dtype",
+        "name": "UInt64Dtype",
+        "children": []
+    },
+    "pandas.Int8Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.Int8Dtype",
+        "name": "Int8Dtype",
+        "children": []
+    },
+    "pandas.Int16Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.Int16Dtype",
+        "name": "Int16Dtype",
+        "children": []
+    },
+    "pandas.Int32Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.Int32Dtype",
+        "name": "Int32Dtype",
+        "children": []
+    },
+    "pandas.Int64Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.Int64Dtype",
+        "name": "Int64Dtype",
+        "children": []
+    },
+    "pandas.Float32Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.Float32Dtype",
+        "name": "Float32Dtype",
+        "children": []
+    },
+    "pandas.Float64Dtype": {
+        "type": "attribute",
+        "full_path": "pandas.Float64Dtype",
+        "name": "Float64Dtype",
+        "children": []
+    },
     "datetime": {
         "type": "module",
         "full_path": "datetime",
@@ -281,7 +358,8 @@
         "full_path": "numpy.ma",
         "name": "ma",
         "children": [
-            "numpy.ma.masked"
+            "numpy.ma.masked",
+            "numpy.ma.masked_array"
         ]
     },
     "numpy.ma.masked": {
@@ -290,6 +368,12 @@
         "name": "masked",
         "children": []
     },
+    "numpy.ma.masked_array": {
+        "type": "attribute",
+        "full_path": "numpy.ma.masked_array",
+        "name": "masked_array",
+        "children": []
+    },
     "numpy.ndarray": {
         "type": "attribute",
         "full_path": "numpy.ndarray",
diff --git a/tools/pythonpkg/scripts/imports.py b/tools/pythonpkg/scripts/imports.py
index f34844280080..cfa45d65a13c 100644
--- a/tools/pythonpkg/scripts/imports.py
+++ b/tools/pythonpkg/scripts/imports.py
@@ -13,6 +13,17 @@
 pandas.NA
 pandas.isnull
 pandas.ArrowDtype
+pandas.BooleanDtype
+pandas.UInt8Dtype
+pandas.UInt16Dtype
+pandas.UInt32Dtype
+pandas.UInt64Dtype
+pandas.Int8Dtype
+pandas.Int16Dtype
+pandas.Int32Dtype
+pandas.Int64Dtype
+pandas.Float32Dtype
+pandas.Float64Dtype
 
 import datetime
 
@@ -46,6 +57,7 @@
 
 numpy.core.multiarray
 numpy.ma.masked
+numpy.ma.masked_array
 numpy.ndarray
 numpy.datetime64
 numpy.generic
diff --git a/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/numpy_module.hpp b/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/numpy_module.hpp
index fbfaf52cc462..7ec78a883842 100644
--- a/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/numpy_module.hpp
+++ b/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/numpy_module.hpp
@@ -17,12 +17,13 @@ struct NumpyMaCacheItem : public PythonImportCacheItem {
 
 public:
 	NumpyMaCacheItem(optional_ptr<PythonImportCacheItem> parent)
-	    : PythonImportCacheItem("ma", parent), masked("masked", this) {
+	    : PythonImportCacheItem("ma", parent), masked("masked", this), masked_array("masked_array", this) {
 	}
 	~NumpyMaCacheItem() override {
 	}
 
 	PythonImportCacheItem masked;
+	PythonImportCacheItem masked_array;
 };
 
 struct NumpyCoreCacheItem : public PythonImportCacheItem {
diff --git a/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/pandas_module.hpp b/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/pandas_module.hpp
index 30c36f691233..dc13c129a2d9 100644
--- a/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/pandas_module.hpp
+++ b/tools/pythonpkg/src/include/duckdb_python/import_cache/modules/pandas_module.hpp
@@ -21,7 +21,11 @@ struct PandasCacheItem : public PythonImportCacheItem {
 public:
 	PandasCacheItem()
 	    : PythonImportCacheItem("pandas"), DataFrame("DataFrame", this), isnull("isnull", this),
-	      ArrowDtype("ArrowDtype", this), NaT("NaT", this), NA("NA", this) {
+	      ArrowDtype("ArrowDtype", this), NaT("NaT", this), NA("NA", this), BooleanDtype("BooleanDtype", this),
+	      UInt8Dtype("UInt8Dtype", this), UInt16Dtype("UInt16Dtype", this), UInt32Dtype("UInt32Dtype", this),
+	      UInt64Dtype("UInt64Dtype", this), Int8Dtype("Int8Dtype", this), Int16Dtype("Int16Dtype", this),
+	      Int32Dtype("Int32Dtype", this), Int64Dtype("Int64Dtype", this), Float32Dtype("Float32Dtype", this),
+	      Float64Dtype("Float64Dtype", this) {
 	}
 	~PandasCacheItem() override {
 	}
@@ -31,6 +35,17 @@ struct PandasCacheItem : public PythonImportCacheItem {
 	PythonImportCacheItem ArrowDtype;
 	PythonImportCacheItem NaT;
 	PythonImportCacheItem NA;
+	PythonImportCacheItem BooleanDtype;
+	PythonImportCacheItem UInt8Dtype;
+	PythonImportCacheItem UInt16Dtype;
+	PythonImportCacheItem UInt32Dtype;
+	PythonImportCacheItem UInt64Dtype;
+	PythonImportCacheItem Int8Dtype;
+	PythonImportCacheItem Int16Dtype;
+	PythonImportCacheItem Int32Dtype;
+	PythonImportCacheItem Int64Dtype;
+	PythonImportCacheItem Float32Dtype;
+	PythonImportCacheItem Float64Dtype;
 
 protected:
 	bool IsRequired() const override final {
diff --git a/tools/pythonpkg/src/include/duckdb_python/pyresult.hpp b/tools/pythonpkg/src/include/duckdb_python/pyresult.hpp
index b889f108d99b..2e6b83070604 100644
--- a/tools/pythonpkg/src/include/duckdb_python/pyresult.hpp
+++ b/tools/pythonpkg/src/include/duckdb_python/pyresult.hpp
@@ -65,7 +65,6 @@ struct DuckDBPyResult {
 	PandasDataFrame FrameFromNumpy(bool date_as_object, const py::handle &o);
 
 	void ChangeToTZType(PandasDataFrame &df);
-	void ChangeDateToDatetime(PandasDataFrame &df);
 	unique_ptr<DataChunk> FetchNext(QueryResult &result);
 	unique_ptr<DataChunk> FetchNextRaw(QueryResult &result);
 	unique_ptr<NumpyResultConversion> InitializeNumpyConversion(bool pandas = false);
diff --git a/tools/pythonpkg/src/numpy/array_wrapper.cpp b/tools/pythonpkg/src/numpy/array_wrapper.cpp
index 6f45d344b9f5..94b283bc2d11 100644
--- a/tools/pythonpkg/src/numpy/array_wrapper.cpp
+++ b/tools/pythonpkg/src/numpy/array_wrapper.cpp
@@ -567,10 +567,12 @@ static bool ConvertDecimalInternal(NumpyAppendData &append_data, double division
 	auto src_ptr = UnifiedVectorFormat::GetData<DUCKDB_T>(idata);
 	auto out_ptr = reinterpret_cast<double *>(target_data);
 	if (!idata.validity.AllValid()) {
+		bool requires_mask = false;
 		for (idx_t i = 0; i < count; i++) {
 			idx_t src_idx = idata.sel->get_index(i + source_offset);
 			idx_t offset = target_offset + i;
 			if (!idata.validity.RowIsValidUnsafe(src_idx)) {
+				requires_mask = true;
 				target_mask[offset] = true;
 			} else {
 				out_ptr[offset] =
@@ -579,7 +581,7 @@ static bool ConvertDecimalInternal(NumpyAppendData &append_data, double division
 				target_mask[offset] = false;
 			}
 		}
-		return true;
+		return requires_mask;
 	} else {
 		for (idx_t i = 0; i < count; i++) {
 			idx_t src_idx = idata.sel->get_index(i + source_offset);
diff --git a/tools/pythonpkg/src/pyresult.cpp b/tools/pythonpkg/src/pyresult.cpp
index a61bb0c7dd96..f20c68de7ca3 100644
--- a/tools/pythonpkg/src/pyresult.cpp
+++ b/tools/pythonpkg/src/pyresult.cpp
@@ -2,6 +2,7 @@
 #include "duckdb_python/pyconnection/pyconnection.hpp"
 #include "duckdb_python/pyresult.hpp"
 #include "duckdb_python/python_objects.hpp"
+#include "duckdb_python/numpy/numpy_type.hpp"
 
 #include "duckdb_python/arrow/arrow_array_stream.hpp"
 #include "duckdb/common/arrow/arrow.hpp"
@@ -288,23 +289,85 @@ void DuckDBPyResult::ChangeToTZType(PandasDataFrame &df) {
 	}
 }
 
-// TODO: unify these with an enum/flag to indicate which conversions to perform
-void DuckDBPyResult::ChangeDateToDatetime(PandasDataFrame &df) {
-	auto names = df.attr("columns").cast<vector<string>>();
+static py::object ConvertNumpyDtype(py::handle numpy_array) {
+	D_ASSERT(py::gil_check());
+	auto &import_cache = *DuckDBPyConnection::ImportCache();
 
-	for (idx_t i = 0; i < result->ColumnCount(); i++) {
-		if (result->types[i] == LogicalType::DATE) {
-			df.attr("__setitem__")(names[i].c_str(), df[names[i].c_str()].attr("dt").attr("date"));
-		}
+	auto dtype = numpy_array.attr("dtype");
+	if (!py::isinstance(numpy_array, import_cache.numpy.ma.masked_array())) {
+		return dtype;
+	}
+
+	auto numpy_type = ConvertNumpyType(dtype);
+	switch (numpy_type.type) {
+	case NumpyNullableType::BOOL: {
+		return import_cache.pandas.BooleanDtype()();
+	}
+	case NumpyNullableType::UINT_8: {
+		return import_cache.pandas.UInt8Dtype()();
+	}
+	case NumpyNullableType::UINT_16: {
+		return import_cache.pandas.UInt16Dtype()();
+	}
+	case NumpyNullableType::UINT_32: {
+		return import_cache.pandas.UInt32Dtype()();
+	}
+	case NumpyNullableType::UINT_64: {
+		return import_cache.pandas.UInt64Dtype()();
+	}
+	case NumpyNullableType::INT_8: {
+		return import_cache.pandas.Int8Dtype()();
+	}
+	case NumpyNullableType::INT_16: {
+		return import_cache.pandas.Int16Dtype()();
+	}
+	case NumpyNullableType::INT_32: {
+		return import_cache.pandas.Int32Dtype()();
+	}
+	case NumpyNullableType::INT_64: {
+		return import_cache.pandas.Int64Dtype()();
+	}
+	case NumpyNullableType::FLOAT_32:
+	case NumpyNullableType::FLOAT_64:
+	case NumpyNullableType::FLOAT_16: // there is no pandas.Float16Dtype
+	default:
+		return dtype;
 	}
 }
 
 PandasDataFrame DuckDBPyResult::FrameFromNumpy(bool date_as_object, const py::handle &o) {
-	PandasDataFrame df = py::cast<PandasDataFrame>(py::module::import("pandas").attr("DataFrame").attr("from_dict")(o));
+	D_ASSERT(py::gil_check());
+	auto &import_cache = *DuckDBPyConnection::ImportCache();
+	auto pandas = import_cache.pandas();
+
+	py::object items = o.attr("items")();
+	for (const py::handle &item : items) {
+		// Each item is a tuple of (key, value)
+		auto key_value = py::cast<py::tuple>(item);
+		py::handle key = key_value[0];   // Access the first element (key)
+		py::handle value = key_value[1]; // Access the second element (value)
+
+		auto dtype = ConvertNumpyDtype(value);
+		if (py::isinstance(value, import_cache.numpy.ma.masked_array())) {
+			// o[key] = pd.Series(value.filled(pd.NA), dtype=dtype)
+			auto series = pandas.attr("Series")(value.attr("data"), py::arg("dtype") = dtype);
+			series.attr("__setitem__")(value.attr("mask"), import_cache.pandas.NA());
+			o.attr("__setitem__")(key, series);
+		}
+	}
+
+	PandasDataFrame df = py::cast<PandasDataFrame>(pandas.attr("DataFrame").attr("from_dict")(o));
 	// Unfortunately we have to do a type change here for timezones since these types are not supported by numpy
 	ChangeToTZType(df);
+
+	auto names = df.attr("columns").cast<vector<string>>();
+	D_ASSERT(result->ColumnCount() == names.size());
 	if (date_as_object) {
-		ChangeDateToDatetime(df);
+		for (idx_t i = 0; i < result->ColumnCount(); i++) {
+			if (result->types[i] == LogicalType::DATE) {
+				df.attr("__setitem__")(names[i].c_str(), df[names[i].c_str()].attr("dt").attr("date"));
+			}
+		}
 	}
 	return df;
 }


Test patch:
diff --git a/tools/pythonpkg/tests/fast/api/test_dbapi00.py b/tools/pythonpkg/tests/fast/api/test_dbapi00.py
index e8614864814e..141ff4c42897 100644
--- a/tools/pythonpkg/tests/fast/api/test_dbapi00.py
+++ b/tools/pythonpkg/tests/fast/api/test_dbapi00.py
@@ -94,7 +94,7 @@ def test_pandas_selection(self, duckdb_cursor, pandas, integers, timestamps):
         result = duckdb_cursor.fetchdf()
         arr = numpy.ma.masked_array(numpy.arange(11))
         arr.mask = [False] * 10 + [True]
-        arr = {'i': arr}
+        arr = {'i': pandas.Series(arr, dtype=pandas.Int32Dtype)}
         arr = pandas.DataFrame(arr)
         pandas.testing.assert_frame_equal(result, arr)
 
diff --git a/tools/pythonpkg/tests/fast/pandas/test_fetch_nested.py b/tools/pythonpkg/tests/fast/pandas/test_fetch_nested.py
index 641d4d119656..060974e7a018 100644
--- a/tools/pythonpkg/tests/fast/pandas/test_fetch_nested.py
+++ b/tools/pythonpkg/tests/fast/pandas/test_fetch_nested.py
@@ -433,8 +433,8 @@ def test_map_df(self, duckdb_cursor, query, expected, expected_error):
             'a': [
                 {'i': 1, 'j': 2},
                 {'i': 1, 'j': 2},
-                np.nan,
-                np.nan
+                pd.NA,
+                pd.NA
             ]
         }),
         ("""
@@ -454,8 +454,8 @@ def test_map_df(self, duckdb_cursor, query, expected, expected_error):
                     '1':3,
                     '2':4
                 },
-                np.nan,
-                np.nan
+                pd.NA,
+                pd.NA
             ]
         }),
     ])
diff --git a/tools/pythonpkg/tests/fast/pandas/test_pandas_category.py b/tools/pythonpkg/tests/fast/pandas/test_pandas_category.py
index 59e28c907bd9..e86a97d9467f 100644
--- a/tools/pythonpkg/tests/fast/pandas/test_pandas_category.py
+++ b/tools/pythonpkg/tests/fast/pandas/test_pandas_category.py
@@ -70,7 +70,7 @@ def test_category_nulls(self, duckdb_cursor):
         print(duckdb.query_df(df_in, "data", "SELECT * FROM data").fetchall())
         assert df_out['int'][0] == 1
         assert df_out['int'][1] == 2
-        assert numpy.isnan(df_out['int'][2])
+        assert pd.isna(df_out['int'][2])
 
     def test_category_string(self, duckdb_cursor):
         check_category_equal(['foo', 'bla', 'zoo', 'foo', 'foo', 'bla'])
diff --git a/tools/pythonpkg/tests/fast/pandas/test_pandas_types.py b/tools/pythonpkg/tests/fast/pandas/test_pandas_types.py
index b5f5d6327bec..aeb33ea4601b 100644
--- a/tools/pythonpkg/tests/fast/pandas/test_pandas_types.py
+++ b/tools/pythonpkg/tests/fast/pandas/test_pandas_types.py
@@ -4,6 +4,8 @@
 import numpy
 import string
 from packaging import version
+import warnings
+from contextlib import suppress
 
 
 def round_trip(data, pandas_type):
@@ -106,9 +108,9 @@ def test_pandas_boolean(self, duckdb_cursor):
 
         df_out = duckdb.query_df(df_in, "data", "SELECT * FROM data").df()
         assert df_out['object'][0] == df_in['object'][0]
-        assert numpy.isnan(df_out['object'][1])
-        assert numpy.isnan(df_out['object'][2])
-        assert numpy.isnan(df_out['object'][3])
+        assert pd.isna(df_out['object'][1])
+        assert pd.isna(df_out['object'][2])
+        assert pd.isna(df_out['object'][3])
         assert df_out['object'][4] == df_in['object'][4]
 
     def test_pandas_float32(self, duckdb_cursor):
@@ -124,7 +126,7 @@ def test_pandas_float32(self, duckdb_cursor):
         assert df_out['object'][0] == df_in['object'][0]
         assert df_out['object'][1] == df_in['object'][1]
         assert df_out['object'][2] == df_in['object'][2]
-        assert numpy.isnan(df_out['object'][3])
+        assert pd.isna(df_out['object'][3])
 
     def test_pandas_float64(self):
         data = numpy.array([0.233, numpy.nan, 3456.2341231, float('-inf'), -23424.45345, float('+inf'), 0.0000000001])
@@ -136,7 +138,7 @@ def test_pandas_float64(self):
         df_out = duckdb.query_df(df_in, "data", "SELECT * FROM data").df()
 
         for i in range(len(data)):
-            if numpy.isnan(df_out['object'][i]):
+            if pd.isna(df_out['object'][i]):
                 assert i == 1
                 continue
             assert df_out['object'][i] == df_in['object'][i]
@@ -164,3 +166,69 @@ def test_pandas_encoded_utf8(self, duckdb_cursor):
         df_in = pd.DataFrame({'object': pd.Series(data, dtype='object')})
         result = duckdb.query_df(df_in, "data", "SELECT * FROM data").fetchone()[0]
         assert result == expected_result
+
+    @pytest.mark.parametrize(
+        'dtype',
+        [
+            'bool',
+            'utinyint',
+            'usmallint',
+            'uinteger',
+            'ubigint',
+            'tinyint',
+            'smallint',
+            'integer',
+            'bigint',
+            'float',
+            'double',
+        ],
+    )
+    def test_producing_nullable_dtypes(self, duckdb_cursor, dtype):
+        class Input:
+            def __init__(self, value, expected_dtype):
+                self.value = value
+                self.expected_dtype = expected_dtype
+
+        inputs = {
+            'bool': Input('true', 'BooleanDtype'),
+            'utinyint': Input('255', 'UInt8Dtype'),
+            'usmallint': Input('65535', 'UInt16Dtype'),
+            'uinteger': Input('4294967295', 'UInt32Dtype'),
+            'ubigint': Input('18446744073709551615', 'UInt64Dtype'),
+            'tinyint': Input('-128', 'Int8Dtype'),
+            'smallint': Input('-32768', 'Int16Dtype'),
+            'integer': Input('-2147483648', 'Int32Dtype'),
+            'bigint': Input('-9223372036854775808', 'Int64Dtype'),
+            'float': Input('268043421344044473239570760152672894976.0000000000', 'float32'),
+            'double': Input(
+                '14303088389124869511075243108389716684037132417196499782261853698893384831666205572097390431189931733040903060865714975797777061496396865611606109149583360363636503436181348332896211726552694379264498632046075093077887837955077425420408952536212326792778411457460885268567735875437456412217418386401944141824.0000000000',
+                'float64',
+            ),
+        }
+
+        input = inputs[dtype]
+        if not hasattr(pd, input.expected_dtype) and not hasattr(numpy, input.expected_dtype):
+            pytest.skip("Could not test this nullable type, the version of pandas does not provide it")
+
+        query = f"""
+            select
+                a::{dtype} a
+            from (VALUES
+                (NULL),
+                ({input.value}),
+                (NULL)
+            ) t(a)
+        """
+
+        rel = duckdb_cursor.sql(query)
+        # Pandas <= 2.2.3 does not convert without throwing a warning
+        warnings.simplefilter(action='ignore', category=RuntimeWarning)
+        with suppress(TypeError):
+            df = rel.df()
+            warnings.resetwarnings()
+
+            if hasattr(pd, input.expected_dtype):
+                expected_dtype = getattr(pd, input.expected_dtype)
+            else:
+                expected_dtype = numpy.dtype(input.expected_dtype)
+            assert isinstance(df['a'].dtype, expected_dtype)
diff --git a/tools/pythonpkg/tests/fast/test_all_types.py b/tools/pythonpkg/tests/fast/test_all_types.py
index f7ab55178113..ce56165e428d 100644
--- a/tools/pythonpkg/tests/fast/test_all_types.py
+++ b/tools/pythonpkg/tests/fast/test_all_types.py
@@ -7,6 +7,8 @@
 from uuid import UUID
 import pytz
 import pytest
+import warnings
+from contextlib import suppress
 
 
 def replace_with_ndarray(obj):
@@ -582,16 +584,20 @@ def test_pandas(self, cur_type):
             'time': """CASE WHEN "time" = '24:00:00'::TIME THEN '23:59:59.999999'::TIME ELSE "time" END AS "time" """,
         }
         conn = duckdb.connect()
+        # Pandas <= 2.2.3 does not convert without throwing a warning
         conn.execute("SET timezone = UTC")
-        if cur_type in replacement_values:
-            dataframe = conn.execute("select " + replacement_values[cur_type]).df()
-        elif cur_type in adjusted_values:
-            dataframe = conn.execute(f'select {adjusted_values[cur_type]} from test_all_types()').df()
-        else:
-            dataframe = conn.execute(f'select "{cur_type}" from test_all_types()').df()
-        print(cur_type)
-        round_trip_dataframe = conn.execute("select * from dataframe").df()
-        result_dataframe = conn.execute("select * from dataframe").fetchall()
-        print(round_trip_dataframe)
-        result_roundtrip = conn.execute("select * from round_trip_dataframe").fetchall()
-        assert recursive_equality(result_dataframe, result_roundtrip)
+        warnings.simplefilter(action='ignore', category=RuntimeWarning)
+        with suppress(TypeError):
+            if cur_type in replacement_values:
+                dataframe = conn.execute("select " + replacement_values[cur_type]).df()
+            elif cur_type in adjusted_values:
+                dataframe = conn.execute(f'select {adjusted_values[cur_type]} from test_all_types()').df()
+            else:
+                dataframe = conn.execute(f'select "{cur_type}" from test_all_types()').df()
+            print(cur_type)
+            round_trip_dataframe = conn.execute("select * from dataframe").df()
+            result_dataframe = conn.execute("select * from dataframe").fetchall()
+            print(round_trip_dataframe)
+            result_roundtrip = conn.execute("select * from round_trip_dataframe").fetchall()
+            warnings.resetwarnings()
+            assert recursive_equality(result_dataframe, result_roundtrip)
diff --git a/tools/pythonpkg/tests/fast/types/test_object_int.py b/tools/pythonpkg/tests/fast/types/test_object_int.py
index d22a63d41e9a..ce153d497730 100644
--- a/tools/pythonpkg/tests/fast/types/test_object_int.py
+++ b/tools/pythonpkg/tests/fast/types/test_object_int.py
@@ -2,6 +2,8 @@
 import datetime
 import duckdb
 import pytest
+import warnings
+from contextlib import suppress
 
 
 class TestPandasObjectInteger(object):
@@ -16,40 +18,47 @@ def test_object_integer(self, duckdb_cursor):
                 'int64': pd.Series([None, 1, -1], dtype="Int64"),
             }
         )
-        # These are float64 because pandas would force these to be float64 even if we set them to int8, int16, int32, int64 respectively
+        warnings.simplefilter(action='ignore', category=RuntimeWarning)
         df_expected_res = pd.DataFrame(
             {
-                'int8': np.ma.masked_array([0, 1, -1], mask=[True, False, False], dtype='float64'),
-                'int16': np.ma.masked_array([0, 1, -1], mask=[True, False, False], dtype='float64'),
-                'int32': np.ma.masked_array([0, 1, -1], mask=[True, False, False], dtype='float64'),
-                'int64': np.ma.masked_array([0, 1, -1], mask=[True, False, False], dtype='float64'),
+                'int8': pd.Series(np.ma.masked_array([0, 1, -1], mask=[True, False, False]), dtype='Int8'),
+                'int16': pd.Series(np.ma.masked_array([0, 1, -1], mask=[True, False, False]), dtype='Int16'),
+                'int32': pd.Series(np.ma.masked_array([0, 1, -1], mask=[True, False, False]), dtype='Int32'),
+                'int64': pd.Series(np.ma.masked_array([0, 1, -1], mask=[True, False, False]), dtype='Int64'),
             }
         )
         df_out = duckdb.query_df(df_in, "data", "SELECT * FROM data").df()
+        warnings.resetwarnings()
         pd.testing.assert_frame_equal(df_expected_res, df_out)
 
     # Unsigned Masked Integer types
     def test_object_uinteger(self, duckdb_cursor):
         pd = pytest.importorskip("pandas")
-        df_in = pd.DataFrame(
-            {
-                'uint8': pd.Series([None, 1, 255], dtype="UInt8"),
-                'uint16': pd.Series([None, 1, 65535], dtype="UInt16"),
-                'uint32': pd.Series([None, 1, 4294967295], dtype="UInt32"),
-                'uint64': pd.Series([None, 1, 18446744073709551615], dtype="UInt64"),
-            }
-        )
-        # These are float64 because pandas would force these to be float64 even if we set them to uint8, uint16, uint32, uint64 respectively
-        df_expected_res = pd.DataFrame(
-            {
-                'uint8': np.ma.masked_array([0, 1, 255], mask=[True, False, False], dtype='float64'),
-                'uint16': np.ma.masked_array([0, 1, 65535], mask=[True, False, False], dtype='float64'),
-                'uint32': np.ma.masked_array([0, 1, 4294967295], mask=[True, False, False], dtype='float64'),
-                'uint64': np.ma.masked_array([0, 1, 18446744073709551615], mask=[True, False, False], dtype='float64'),
-            }
-        )
-        df_out = duckdb.query_df(df_in, "data", "SELECT * FROM data").df()
-        pd.testing.assert_frame_equal(df_expected_res, df_out)
+        with suppress(TypeError):
+            df_in = pd.DataFrame(
+                {
+                    'uint8': pd.Series([None, 1, 255], dtype="UInt8"),
+                    'uint16': pd.Series([None, 1, 65535], dtype="UInt16"),
+                    'uint32': pd.Series([None, 1, 4294967295], dtype="UInt32"),
+                    'uint64': pd.Series([None, 1, 18446744073709551615], dtype="UInt64"),
+                }
+            )
+            warnings.simplefilter(action='ignore', category=RuntimeWarning)
+            df_expected_res = pd.DataFrame(
+                {
+                    'uint8': pd.Series(np.ma.masked_array([0, 1, 255], mask=[True, False, False]), dtype='UInt8'),
+                    'uint16': pd.Series(np.ma.masked_array([0, 1, 65535], mask=[True, False, False]), dtype='UInt16'),
+                    'uint32': pd.Series(
+                        np.ma.masked_array([0, 1, 4294967295], mask=[True, False, False]), dtype='UInt32'
+                    ),
+                    'uint64': pd.Series(
+                        np.ma.masked_array([0, 1, 18446744073709551615], mask=[True, False, False]), dtype='UInt64'
+                    ),
+                }
+            )
+            df_out = duckdb.query_df(df_in, "data", "SELECT * FROM data").df()
+            warnings.resetwarnings()
+            pd.testing.assert_frame_equal(df_expected_res, df_out)
 
     # Unsigned Masked float/double types
     def test_object_float(self, duckdb_cursor):
@@ -63,8 +72,12 @@ def test_object_float(self, duckdb_cursor):
         )
         df_expected_res = pd.DataFrame(
             {
-                'float32': np.ma.masked_array([0, 1, 4294967295], mask=[True, False, False], dtype='float32'),
-                'float64': np.ma.masked_array([0, 1, 18446744073709551615], mask=[True, False, False], dtype='float64'),
+                'float32': pd.Series(
+                    np.ma.masked_array([0, 1, 4294967295], mask=[True, False, False]), dtype='float32'
+                ),
+                'float64': pd.Series(
+                    np.ma.masked_array([0, 1, 18446744073709551615], mask=[True, False, False]), dtype='float64'
+                ),
             }
         )
         df_out = duckdb.query_df(df_in, "data", "SELECT * FROM data").df()