Repository: pola-rs/polars (language: python)

Issue:
`fill_null` only fills Int32 columns when filling with an untyped integer literal
### Checks

- [X] I have checked that this issue has not already been reported.
- [X] I have confirmed this bug exists on the [latest version](https://pypi.org/project/polars/) of Polars.

### Reproducible example

```python
import polars as pl

df = pl.DataFrame({
    'a': [1, None],
    'b': [None, 2],
})

# Returns a DataFrame that still contains nulls:
print(df.fill_null(pl.lit(0)))

# Works and fills with zero:
print(df.fill_null(pl.lit(0, dtype=pl.Int64)))
```


### Log output

```shell
shape: (2, 2)
┌──────┬──────┐
│ a    ┆ b    │
│ ---  ┆ ---  │
│ i64  ┆ i64  │
╞══════╪══════╡
│ 1    ┆ null │
│ null ┆ 2    │
└──────┴──────┘
shape: (2, 2)
┌─────┬─────┐
│ a   ┆ b   │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1   ┆ 0   │
│ 0   ┆ 2   │
└─────┴─────┘
```


### Issue description

-

### Expected behavior

If this is unsupported then an error could be thrown, but it seems reasonable that this should work and fill nulls with zeros.

### Installed versions

<details>

```
--------Version info---------
Polars:              1.12.0
Index type:          UInt32
Platform:            Linux-6.11.5-200.fc40.x86_64-x86_64-with-glibc2.39
Python:              3.12.7 (main, Oct  1 2024, 00:00:00) [GCC 14.2.1 20240912 (Red Hat 14.2.1-3)]
LTS CPU:             False

----Optional dependencies----
adbc_driver_manager  <not installed>
altair               <not installed>
cloudpickle          <not installed>
connectorx           <not installed>
deltalake            <not installed>
fastexcel            <not installed>
fsspec               2024.6.1
gevent               <not installed>
great_tables         <not installed>
matplotlib           3.8.2
nest_asyncio         1.5.8
numpy                1.26.3
openpyxl             <not installed>
pandas               2.2.0
pyarrow              17.0.0
pydantic             <not installed>
pyiceberg            <not installed>
sqlalchemy           <not installed>
torch                2.4.1+cu121
xlsx2csv             <not installed>
xlsxwriter           <not installed>
```

</details>



Discussion / hints:
It looks like the literal is treated as an `Int32` and only columns with a matching type are filled:
```python
df = pl.DataFrame({
    'a': pl.Series([1, None], dtype=pl.Int32),
    'b': pl.Series([None, 2], dtype=pl.UInt32),
    'c': pl.Series([None, 2], dtype=pl.Int64),
})
print(df.fill_null(pl.lit(0)))
print(df.fill_null(pl.lit(0, dtype=pl.Int64)))
```

I had thought that `matches_supertype=True` would mean that an int32 literal would work with an int64 column, but maybe I haven't understood what that parameter does?

```
shape: (2, 3)
┌─────┬──────┬──────┐
│ a   ┆ b    ┆ c    │
│ --- ┆ ---  ┆ ---  │
│ i32 ┆ u32  ┆ i64  │
╞═════╪══════╪══════╡
│ 1   ┆ null ┆ null │
│ 0   ┆ 2    ┆ 2    │
└─────┴──────┴──────┘
shape: (2, 3)
┌──────┬──────┬─────┐
│ a    ┆ b    ┆ c   │
│ ---  ┆ ---  ┆ --- │
│ i32  ┆ u32  ┆ i64 │
╞══════╪══════╪═════╡
│ 1    ┆ null ┆ 0   │
│ null ┆ 2    ┆ 2   │
└──────┴──────┴─────┘
```

Passing a Python int as the fill value seems to handle any integer typed column though:
```python
print(df.fill_null(0))
```
```
shape: (2, 3)
┌─────┬─────┬─────┐
│ a   ┆ b   ┆ c   │
│ --- ┆ --- ┆ --- │
│ i32 ┆ u32 ┆ i64 │
╞═════╪═════╪═════╡
│ 1   ┆ 0   ┆ 0   │
│ 0   ┆ 2   ┆ 2   │
└─────┴─────┴─────┘
```

Fix patch:
diff --git a/py-polars/polars/lazyframe/frame.py b/py-polars/polars/lazyframe/frame.py
index a72146414b59..9b438ae8dbfa 100644
--- a/py-polars/polars/lazyframe/frame.py
+++ b/py-polars/polars/lazyframe/frame.py
@@ -5667,7 +5667,7 @@ def fill_null(
         │ 4   ┆ 13.0 │
         └─────┴──────┘
         """
-        dtypes: Sequence[PolarsDataType]
+        dtypes: Sequence[PolarsDataType] | None
 
         if value is not None:
 
@@ -5675,7 +5675,7 @@ def infer_dtype(value: Any) -> PolarsDataType:
                 return next(iter(self.select(value).collect_schema().values()))
 
             if isinstance(value, pl.Expr):
-                dtypes = [infer_dtype(value)]
+                dtypes = None
             elif isinstance(value, bool):
                 dtypes = [Boolean]
             elif matches_supertype and isinstance(value, (int, float)):
@@ -5707,9 +5707,12 @@ def infer_dtype(value: Any) -> PolarsDataType:
                 dtypes = [String, Categorical]
             else:
                 # fallback; anything not explicitly handled above
-                dtypes = [infer_dtype(F.lit(value))]
+                dtypes = None
 
-            return self.with_columns(F.col(dtypes).fill_null(value, strategy, limit))
+            if dtypes:
+                return self.with_columns(
+                    F.col(dtypes).fill_null(value, strategy, limit)
+                )
 
         return self.select(F.all().fill_null(value, strategy, limit))
 


Test patch:
diff --git a/py-polars/tests/unit/operations/test_fill_null.py b/py-polars/tests/unit/operations/test_fill_null.py
index 3b2d5210f75a..3bab4beb91d5 100644
--- a/py-polars/tests/unit/operations/test_fill_null.py
+++ b/py-polars/tests/unit/operations/test_fill_null.py
@@ -35,3 +35,17 @@ def test_fill_null_f32_with_lit() -> None:
     # ensure the literal integer does not upcast the f32 to an f64
     df = pl.DataFrame({"a": [1.1, 1.2]}, schema=[("a", pl.Float32)])
     assert df.fill_null(value=0).dtypes == [pl.Float32]
+
+
+def test_fill_null_lit_() -> None:
+    df = pl.DataFrame(
+        {
+            "a": pl.Series([1, None], dtype=pl.Int32),
+            "b": pl.Series([None, 2], dtype=pl.UInt32),
+            "c": pl.Series([None, 2], dtype=pl.Int64),
+        }
+    )
+    assert (
+        df.fill_null(pl.lit(0)).select(pl.all().null_count()).transpose().sum().item()
+        == 0
+    )