Repository: unionai-oss/pandera

Issue:
Check schema with Decimal not working in Pandera
Seems like it's not possible to check a decimal with pandera schema:

```python
import pandera as pa

schema = pa.DataFrameSchema({
    'sample': pa.Column(
        pa.Decimal(precision=13, scale=2))
})
```

```python
Traceback (most recent call last):
  File "G:\My Drive\PycharmProjects\Development\venv\lib\site-packages\pandera\engines\pandas_engine.py", line 153, in dtype
    return engine.Engine.dtype(cls, data_type)
  File "G:\My Drive\PycharmProjects\Development\venv\lib\site-packages\pandera\engines\engine.py", line 210, in dtype
    raise TypeError(
TypeError: Data type 'Decimal(13, 2)' not understood by Engine.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "C:\Users\stefa\AppData\Local\Programs\Python\Python39\lib\code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 108, in <module>
  File "G:\My Drive\PycharmProjects\Development\venv\lib\site-packages\pandera\schema_components.py", line 81, in __init__
    super().__init__(
  File "G:\My Drive\PycharmProjects\Development\venv\lib\site-packages\pandera\schemas.py", line 1713, in __init__
    self.dtype = dtype  # type: ignore
  File "G:\My Drive\PycharmProjects\Development\venv\lib\site-packages\pandera\schemas.py", line 1820, in dtype
    self._dtype = pandas_engine.Engine.dtype(value) if value else None
  File "G:\My Drive\PycharmProjects\Development\venv\lib\site-packages\pandera\engines\pandas_engine.py", line 171, in dtype
    np_or_pd_dtype = pd.api.types.pandas_dtype(data_type)
  File "G:\My Drive\PycharmProjects\Development\venv\lib\site-packages\pandas\core\dtypes\common.py", line 1777, in pandas_dtype
    npdtype = np.dtype(dtype)
TypeError: Cannot interpret 'DataType(Decimal(13, 2))' as a data type
```




Discussion:
Altough this might work for some developers:
``````python
import pandera as pa

schema = pa.DataFrameSchema({
    'sample': pa.Column(
        Decimal)
})
``````
Decimal(28,0) and others like for example Decimal(13,1) are not the same and should be covered by the schema check
I encountered this issue as well, both when specifying a custom scale and when trying to use default values
This is definitely a bug!

Looking into a fix...

In the mean time, you can use the `pandas_engine` datatype here https://pandera.readthedocs.io/en/stable/reference/generated/pandera.engines.pandas_engine.Decimal.html#pandera.engines.pandas_engine.Decimal

```python
import pandera as pa
from pandera.engines.pandas_engine import Decimal

schema = pa.DataFrameSchema({
    'sample': pa.Column(Decimal(13, 1))
})
print(schema)
```

Resolving patch:
diff --git a/pandera/dtypes.py b/pandera/dtypes.py
--- a/pandera/dtypes.py
+++ b/pandera/dtypes.py
@@ -3,6 +3,7 @@
 from __future__ import annotations
 
 import dataclasses
+import decimal
 import inspect
 from abc import ABC
 from typing import (
@@ -390,6 +391,11 @@ class Complex64(Complex128):
 DEFAULT_PYTHON_PREC = 28
 
 
+def _scale_to_exp(scale: int) -> decimal.Decimal:
+    scale_fmt = format(10**-scale, f".{scale}f")
+    return decimal.Decimal(scale_fmt)
+
+
 @immutable(init=True)
 class Decimal(_Number):
     """Semantic representation of a decimal data type."""
@@ -402,7 +408,21 @@ class Decimal(_Number):
     scale: int = 0  # default 0 is aligned with pyarrow and various databases.
     """The number of digits after the decimal point."""
 
-    def __init__(self, precision: int = DEFAULT_PYTHON_PREC, scale: int = 0):
+    # pylint: disable=line-too-long
+    rounding: str = dataclasses.field(
+        default_factory=lambda: decimal.getcontext().rounding
+    )
+    """
+    The `rounding mode <https://docs.python.org/3/library/decimal.html#rounding-modes>`__
+    supported by the Python :py:class:`decimal.Decimal` class.
+    """
+
+    def __init__(
+        self,
+        precision: int = DEFAULT_PYTHON_PREC,
+        scale: int = 0,
+        rounding: Optional[str] = None,
+    ):
         super().__init__()
         if precision <= 0:
             raise ValueError(
@@ -414,6 +434,15 @@ def __init__(self, precision: int = DEFAULT_PYTHON_PREC, scale: int = 0):
             )
         object.__setattr__(self, "precision", precision)
         object.__setattr__(self, "scale", scale)
+        object.__setattr__(self, "rounding", rounding)
+        object.__setattr__(
+            self, "_exp", _scale_to_exp(scale) if scale else None
+        )
+        object.__setattr__(
+            self,
+            "_ctx",
+            decimal.Context(prec=precision, rounding=self.rounding),
+        )
 
     def __str__(self) -> str:
         return f"{self.__class__.__name__}({self.precision}, {self.scale})"
diff --git a/pandera/engines/engine.py b/pandera/engines/engine.py
--- a/pandera/engines/engine.py
+++ b/pandera/engines/engine.py
@@ -203,6 +203,13 @@ def dtype(cls: _EngineType, data_type: Any) -> _DataType:
         equivalent_data_type = registry.equivalents.get(data_type)
         if equivalent_data_type is not None:
             return equivalent_data_type
+        elif isinstance(data_type, DataType):
+            # in the case where data_type is a parameterized dtypes.DataType instance that isn't
+            # in the equivalents registry, use its type to get the equivalent, and feed
+            # the parameters into the recognized data type class.
+            equivalent_data_type = registry.equivalents.get(type(data_type))
+            if equivalent_data_type is not None:
+                return type(equivalent_data_type)(**data_type.__dict__)
 
         try:
             return registry.dispatch(data_type)
diff --git a/pandera/engines/pandas_engine.py b/pandera/engines/pandas_engine.py
--- a/pandera/engines/pandas_engine.py
+++ b/pandera/engines/pandas_engine.py
@@ -428,11 +428,6 @@ class FLOAT32(FLOAT64):
 ###############################################################################
 
 
-def _scale_to_exp(scale: int) -> decimal.Decimal:
-    scale_fmt = format(10**-scale, f".{scale}f")
-    return decimal.Decimal(scale_fmt)
-
-
 def _check_decimal(
     pandas_obj: pd.Series,
     precision: Optional[int] = None,
@@ -471,7 +466,9 @@ def _check_decimal(
     return is_valid.to_numpy()
 
 
-@Engine.register_dtype(equivalents=["decimal", decimal.Decimal])
+@Engine.register_dtype(
+    equivalents=["decimal", decimal.Decimal, dtypes.Decimal]
+)
 @immutable(init=True)
 class Decimal(DataType, dtypes.Decimal):
     # pylint:disable=line-too-long
@@ -488,6 +485,11 @@ class Decimal(DataType, dtypes.Decimal):
     rounding: str = dataclasses.field(
         default_factory=lambda: decimal.getcontext().rounding
     )
+    """
+    The `rounding mode <https://docs.python.org/3/library/decimal.html#rounding-modes>`__
+    supported by the Python :py:class:`decimal.Decimal` class.
+    """
+
     _exp: decimal.Decimal = dataclasses.field(init=False)
     _ctx: decimal.Context = dataclasses.field(init=False)
 
@@ -497,16 +499,7 @@ def __init__(  # pylint:disable=super-init-not-called
         scale: int = 0,
         rounding: Optional[str] = None,
     ) -> None:
-        dtypes.Decimal.__init__(self, precision, scale)
-        object.__setattr__(self, "rounding", rounding)
-        object.__setattr__(
-            self, "_exp", _scale_to_exp(scale) if scale else None
-        )
-        object.__setattr__(
-            self,
-            "_ctx",
-            decimal.Context(prec=precision, rounding=self.rounding),
-        )
+        dtypes.Decimal.__init__(self, precision, scale, rounding)
 
     def coerce_value(self, value: Any) -> decimal.Decimal:
         """Coerce an value to a particular type."""


Test patch: