Repository: unionai-oss/pandera

Issue:
Nullability for `pl.Float64` in `pl.DataFrame` fails
**Describe the bug**
The nullability constraint is not enforced in `pl.DataFrame` when the underlying data type is `pl.Float64`. When I provide a `pl.DataFrame` with a float column that contains `None`, it passes.

- [x] I have checked that this issue has not already been reported.
- [x] I have confirmed this bug exists on the latest version of pandera.

#### Code Sample, a copy-pastable example

```python
import polars as pl
import pandera.polars as pa


class Schema(pa.DataFrameModel):
    col: float = pa.Field(nullable=False)


df = pl.DataFrame(data={"col": [1.0, 2.5, None]}, schema={"col": pl.Float64})

Schema.validate(df)


```

#### Expected behavior
The validation should vail as the column `col` contains a `None` value, but the schema requires it to be non-nullable.

#### Desktop (please complete the following information):

 - OS: MacOS
 - Browser: Chrome

#### Additional context
The nullability constraint is correctly enforced if the underlying data type is `int` or `str`.
For `float`, the nullability constraint correctly detects NULL values in the form of `np.nan`.




Discussion:

Resolving patch:
diff --git a/pandera/backends/polars/components.py b/pandera/backends/polars/components.py
--- a/pandera/backends/polars/components.py
+++ b/pandera/backends/polars/components.py
@@ -207,10 +207,9 @@ def check_nullable(
                 )
             ]
 
+        expr = pl.col(schema.selector).is_not_null()
         if is_float_dtype(check_obj, schema.selector):
-            expr = pl.col(schema.selector).is_not_nan()
-        else:
-            expr = pl.col(schema.selector).is_not_null()
+            expr = expr & pl.col(schema.selector).is_not_nan()
 
         isna = check_obj.select(expr)
         passed = isna.select([pl.col("*").all()]).collect()


Test patch:
diff --git a/tests/polars/test_polars_components.py b/tests/polars/test_polars_components.py
--- a/tests/polars/test_polars_components.py
+++ b/tests/polars/test_polars_components.py
@@ -129,7 +129,7 @@ def test_coerce_dtype(data, from_dtype, to_dtype, exception_cls):
 NULLABLE_DTYPES_AND_DATA = [
     [pl.Int64, [1, 2, 3, None]],
     [pl.Utf8, ["foo", "bar", "baz", None]],
-    [pl.Float64, [1.0, 2.0, 3.0, float("nan")]],
+    [pl.Float64, [1.0, 2.0, 3.0, float("nan"), None]],
     [pl.Boolean, [True, False, True, None]],
 ]
 
@@ -138,7 +138,7 @@ def test_coerce_dtype(data, from_dtype, to_dtype, exception_cls):
 @pytest.mark.parametrize("nullable", [True, False])
 def test_check_nullable(dtype, data, nullable):
     data = pl.LazyFrame({"column": pl.Series(data, dtype=dtype)})
-    column_schema = pa.Column(pl.Int64, nullable=nullable, name="column")
+    column_schema = pa.Column(dtype, nullable=nullable, name="column")
     backend = ColumnBackend()
     check_results: List[CoreCheckResult] = backend.check_nullable(
         data, column_schema
@@ -153,9 +153,7 @@ def test_check_nullable_regex(dtype, data, nullable):
     data = pl.LazyFrame(
         {f"column_{i}": pl.Series(data, dtype=dtype) for i in range(3)}
     )
-    column_schema = pa.Column(
-        pl.Int64, nullable=nullable, name=r"^column_\d+$"
-    )
+    column_schema = pa.Column(dtype, nullable=nullable, name=r"^column_\d+$")
     backend = ColumnBackend()
     check_results = backend.check_nullable(data, column_schema)
     for result in check_results: