Repository: unionai-oss/pandera

Issue:
Make dtype optional in Column
Is there a specific reason that the Column's dtype must be specified? If working with a dataset where that doesn't matter (eg. lots of str columns) it would be nice to omit this check.

Any thoughts on this?

https://github.com/pandera-dev/pandera/blob/ffd57d85d4983bb085ca0cd0af1fba59970ff6db/pandera/schema_components.py#L13


Discussion:
I think it's mainly vestigial from when the api was much smaller. Any interest in making a PR? Will prioritize for next release.

Out of curiosity, how often do you have this use case?

Resolving patch:
diff --git a/pandera/schema_components.py b/pandera/schema_components.py
--- a/pandera/schema_components.py
+++ b/pandera/schema_components.py
@@ -4,6 +4,7 @@
 
 import pandas as pd
 
+from . import errors
 from .dtypes import PandasDtype
 from .checks import Check, List
 from .schemas import DataFrameSchema, SeriesSchemaBase
@@ -13,7 +14,7 @@ class Column(SeriesSchemaBase):
 
     def __init__(
             self,
-            pandas_dtype: Union[str, PandasDtype],
+            pandas_dtype: Union[str, PandasDtype] = None,
             checks: Union[Check, List[Check]] = None,
             nullable: bool = False,
             allow_duplicates: bool = True,
@@ -33,6 +34,8 @@ def __init__(
             be coerced into the specified dtype.
         :param required: Whether or not column is allowed to be missing
 
+        :raises SchemaInitError: if impossible to build schema from parameters
+
         :example:
 
         >>> import pandas as pd
@@ -54,6 +57,11 @@ def __init__(
             pandas_dtype, checks, nullable, allow_duplicates)
         self.coerce = coerce
         self.required = required
+        self.pandas_dtype = pandas_dtype
+
+        if coerce and pandas_dtype is None:
+            raise errors.SchemaInitError(
+                "Must specify dtype if coercing a Column's type")
 
     @property
     def _allow_groupby(self):
@@ -99,7 +107,7 @@ class Index(SeriesSchemaBase):
 
     def __init__(
             self,
-            pandas_dtype: Union[str, PandasDtype],
+            pandas_dtype: Union[str, PandasDtype] = None,
             checks: Union[Check, List[Check]] = None,
             nullable: bool = False,
             allow_duplicates: bool = True,
@@ -207,7 +215,7 @@ def __init__(
         super(MultiIndex, self).__init__(
             columns={
                 i if index._name is None else index._name: Column(
-                    index._pandas_dtype,
+                    pandas_dtype=index._pandas_dtype,
                     checks=index._checks,
                     nullable=index._nullable,
                     allow_duplicates=index._allow_duplicates,
diff --git a/pandera/schemas.py b/pandera/schemas.py
--- a/pandera/schemas.py
+++ b/pandera/schemas.py
@@ -41,6 +41,8 @@ def __init__(
         :param strict: whether or not to accept columns in the dataframe that
             aren't in the DataFrameSchema.
 
+        :raises SchemaInitError: if impossible to build schema from parameters
+
         :examples:
 
         >>> import pandera as pa
@@ -79,6 +81,11 @@ def __init__(
             checks = []
         if isinstance(checks, Check):
             checks = [checks]
+
+        if coerce and None in [c.pandas_dtype for c in columns.values()]:
+            raise errors.SchemaInitError(
+                "Must specify dtype in all Columns if coercing DataFrameSchema")
+
         self._checks = checks
         self.index = index
         self.columns = columns
@@ -273,7 +280,7 @@ class SeriesSchemaBase(object):
 
     def __init__(
             self,
-            pandas_dtype: Union[str, dtypes.PandasDtype],
+            pandas_dtype: Union[str, dtypes.PandasDtype] = None,
             checks: callable = None,
             nullable: bool = False,
             allow_duplicates: bool = True,
@@ -321,8 +328,11 @@ def __call__(
             raise errors.SchemaError(
                 "Expected %s to have name '%s', found '%s'" %
                 (type(self), self._name, series.name))
-        expected_dtype = _dtype = self._pandas_dtype if \
-            isinstance(self._pandas_dtype, str) else self._pandas_dtype.value
+
+        expected_dtype = _dtype = self._pandas_dtype if (
+            isinstance(self._pandas_dtype, str) or self._pandas_dtype is None
+        ) else self._pandas_dtype.value
+
         if self._nullable:
             series = series.dropna()
             if dataframe_context is not None:
@@ -365,7 +375,7 @@ def __call__(
                      series[duplicates].head(
                         constants.N_FAILURE_CASES).to_dict()))
 
-        if series.dtype != _dtype:
+        if _dtype is not None and series.dtype != _dtype:
             raise errors.SchemaError(
                 "expected series '%s' to have type %s, got %s" %
                 (series.name, expected_dtype, series.dtype))
@@ -384,7 +394,7 @@ class SeriesSchema(SeriesSchemaBase):
 
     def __init__(
             self,
-            pandas_dtype: dtypes.PandasDtype,
+            pandas_dtype: dtypes.PandasDtype = None,
             checks: List[Check] = None,
             nullable: bool = False,
             allow_duplicates: bool = True,


Test patch:
diff --git a/tests/test_pandera.py b/tests/test_pandera.py
--- a/tests/test_pandera.py
+++ b/tests/test_pandera.py
@@ -502,6 +502,42 @@ def test_coerce_dtype():
             schema.validate(df)
 
 
+def test_no_dtype():
+    schema = DataFrameSchema({"col": Column(nullable=False)})
+    validated_df = schema.validate(pd.DataFrame({"col": [-123.1, -76.3, 1.0]}))
+    assert isinstance(validated_df, pd.DataFrame)
+
+    schema = DataFrameSchema({"col": Column(nullable=True)})
+    validated_df = schema.validate(pd.DataFrame({"col": [-123.1, None, 1.0]}))
+    assert isinstance(validated_df, pd.DataFrame)
+
+    with pytest.raises(errors.SchemaError):
+        schema = DataFrameSchema({"col": Column(nullable=False)})
+        schema.validate(pd.DataFrame({"col": [-123.1, None, 1.0]}))
+
+
+def test_no_dtype_series():
+    schema = SeriesSchema(nullable=False)
+    validated_series = schema.validate(pd.Series([0, 1, 2, 3, 4, 1]))
+    assert isinstance(validated_series, pd.Series)
+
+    schema = SeriesSchema(nullable=True)
+    validated_series = schema.validate(pd.Series([0, 1, 2, None, 4, 1]))
+    assert isinstance(validated_series, pd.Series)
+
+    with pytest.raises(errors.SchemaError):
+        schema = SeriesSchema(nullable=False)
+        schema.validate(pd.Series([0, 1, 2, None, 4, 1]))
+
+
+def test_coerce_without_dtype():
+    with pytest.raises(errors.SchemaInitError):
+        DataFrameSchema({"col": Column(coerce=True)})
+
+    with pytest.raises(errors.SchemaInitError):
+        DataFrameSchema({"col": Column()}, coerce=True)
+
+
 def test_required():
     schema = DataFrameSchema({
         "col1": Column(Int, required=False),