Repository: dask/dask

Issue:
dask.dataframe.map_partitions enforce_metadata not checking for dtypes
It looks like map_partitions(enforce_metadata=True) is not checking dtypes.
The following code should raise because we provide a int64 meta while the mapped function returns strings only.
```python
import dask
import pandas as pd

dd = dask.datasets.timeseries()

def func(df):
    # Force returning strings dtype('O')
    return df.astype(str)

# Should raise dtype('O') != int64
dask.dataframe.map_partitions(func, dd, enforce_metadata=True, meta=dd._meta.astype('int64')).compute()
```

On the other side, from_delayed(verify_meta=True) is raising:
```python
import dask
import pandas as pd

df = dask.datasets.timeseries().head(compute=True)

@dask.delayed
def func():
    # Force returning strings dtype('O')
    return df.astype(str)

# Raise dtype('O') != int64
dask.dataframe.from_delayed([func()], meta=df.head(0).astype('int64')).compute()
```
![image](https://user-images.githubusercontent.com/1694892/188701266-35f1b9a4-c89c-4ea3-85d4-25f95c07e362.png)

I am wondering if this behavior is intended or not? I know enforcing dtypes would be very chalenging when mixing partitions with integers and floats that may easily be upcasted or even with mixed categories....

**Environment**:

- Dask version: 2022.8.0
- Python version: 3.9.12
- Operating System: Windows 10
- Install method (conda, pip, source): pip



Discussion:
Thanks for the issue @epizut - I agree that the behavior of `enforce_metadata` seems to differ from the description in the docstring. More specifically, the `apply_and_enforce` function that is used to wrap the user-defined function only checks that the output [column names and ordering match](https://github.com/dask/dask/blob/b016998fa931f644df4d266a3ed5e7604c20d2a9/dask/dataframe/core.py#L6668) with `meta`.  The docstring, however, clearly states that mismatched types [will lead to an error](https://github.com/dask/dask/blob/b016998fa931f644df4d266a3ed5e7604c20d2a9/dask/dataframe/core.py#L767).

>I am wondering if this behavior is intended or not? I know enforcing dtypes would be very chalenging when mixing partitions with integers and floats that may easily be upcasted or even with mixed categories....

My intuition is that enforcing strict dtype matching would indeed be difficult. Pandas' casting rules are just a bit too complex to capture with the synthetic meta that dask-dataframe typically uses to emulate type evolution.  Therefore, my personal suggestion is that we update the `map_partitions` docstring to more-accurately describe the current behavior of `enforce_metadata`.
Works for me!

Looks like map_overlap() should be updated too.
@epizut Would you be interested in opening a PR to fix these docs? 
> @epizut Would you be interested in opening a PR to fix these docs?

Sure,

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -764,7 +764,8 @@ def map_partitions(self, func, *args, **kwargs):
             Whether to enforce at runtime that the structure of the DataFrame
             produced by ``func`` actually matches the structure of ``meta``.
             This will rename and reorder columns for each partition,
-            and will raise an error if this doesn't work or types don't match.
+            and will raise an error if this doesn't work,
+            but it won't raise if dtypes don't match.
         transform_divisions : bool, default True
             Whether to apply the function onto the divisions and apply those
             transformed divisions to the output.
@@ -897,7 +898,8 @@ def map_overlap(self, func, before, after, *args, **kwargs):
             Whether to enforce at runtime that the structure of the DataFrame
             produced by ``func`` actually matches the structure of ``meta``.
             This will rename and reorder columns for each partition,
-            and will raise an error if this doesn't work or types don't match.
+            and will raise an error if this doesn't work,
+            but it won't raise if dtypes don't match.
         transform_divisions : bool, default True
             Whether to apply the function onto the divisions and apply those
             transformed divisions to the output.
@@ -6476,7 +6478,8 @@ def map_partitions(
         Whether to enforce at runtime that the structure of the DataFrame
         produced by ``func`` actually matches the structure of ``meta``.
         This will rename and reorder columns for each partition,
-        and will raise an error if this doesn't work or types don't match.
+        and will raise an error if this doesn't work,
+        but it won't raise if dtypes don't match.
     transform_divisions : bool, default True
         Whether to apply the function onto the divisions and apply those
         transformed divisions to the output.
diff --git a/dask/dataframe/io/io.py b/dask/dataframe/io/io.py
--- a/dask/dataframe/io/io.py
+++ b/dask/dataframe/io/io.py
@@ -999,7 +999,8 @@ def from_map(
         Whether to enforce at runtime that the structure of the DataFrame
         produced by ``func`` actually matches the structure of ``meta``.
         This will rename and reorder columns for each partition,
-        and will raise an error if this doesn't work or types don't match.
+        and will raise an error if this doesn't work,
+        but it won't raise if dtypes don't match.
     **kwargs:
         Key-word arguments to broadcast to each output partition. These
         same arguments will be passed to ``func`` for every output partition.
diff --git a/dask/dataframe/rolling.py b/dask/dataframe/rolling.py
--- a/dask/dataframe/rolling.py
+++ b/dask/dataframe/rolling.py
@@ -119,7 +119,8 @@ def map_overlap(
         Whether to enforce at runtime that the structure of the DataFrame
         produced by ``func`` actually matches the structure of ``meta``.
         This will rename and reorder columns for each partition,
-        and will raise an error if this doesn't work or types don't match.
+        and will raise an error if this doesn't work,
+        but it won't raise if dtypes don't match.
     before : int or timedelta
         The rows to prepend to partition ``i`` from the end of
         partition ``i - 1``.


Test patch: