Repository: binary-butterfly/validataclass (language: python)

Issue:
Support for mypy
This issue consists of two major parts: First, integrate mypy as a static type checker in the dev and build process of the library (and fix issues that mypy finds). Second, try to find out how to make validataclass **compatible** with mypy (i.e. in projects that use validataclass and mypy).

### Use mypy to check the library code

- [x] Add mypy to the dev environment (tox, Makefile) as an **optional** step for now.
  - Only add `/src` to the target files for now, but add a comment to add `/tests` later too.
  - (The tests will probably require the full validataclass-mypy compatibility.)
- [x] Fix all low-hanging fruit issues detected by mypy (some issues might need a bit more work)
- [x] Try to fix the more complicated issues (if necessary, mark them as `# type: ignore` for now, maybe create issues for them)
- [x] When all issues are resolved or ignored, make mypy a **required** (i.e. default) step in the Tox config.
- [x] Integrate mypy into the test and build CI pipelines
- [x] Fix typing issues in the unit tests that are **not** related to how typing in validataclasses works (don't permanently add `/tests` to the mypy config yet)

### Make validataclass fully mypy-compatible

- [x] Make package PEP 561 compatible: #125
- [ ] Run mypy on validataclass example code (e.g. the unit tests) and examine the issues.
  - Assigning validators to fields in validataclasses: `foo: int = IntegerValidator()` - it's a validator, not an int.
  - Subclasses of validataclasses with incompatible field type overrides (e.g. subclass changes field type from `int` to `OptionalUnset[int]`)
  - Sentinels: Checking `x is not UnsetValue` does not narrow down the type, and evaluating as boolean `if x:` doesn't either. (For the latter: would it help to declare the type of `UnsetValueType.__bool__` as `Literal[False]`?)
- [ ] Write mypy plugin.
- [ ] ???
- [ ] Tests and documentation
  - [ ] Document how to use mypy with validataclass (best practices etc.)
Integrate mypy into development process (part of #116)
This PR integrates mypy as a static type checker into the development environment and the CI pipeline. To do that, it fixes a bunch of typing issues in the library code. It solves a part of #116 (but not all of it as that would be too big of a scope).

List of general changes:

- Add mypy to dev environment (tox, Makefile)
- Add mypy to CI pipeline (GitHub Actions)
- Set mypy rules to strict (apart from `no_implicit_reexport`)

List of code changes:

- Add a lot of missing type hints and fix some incorrect/inaccurate ones
- Replace some type checks with more type-safe ones (e.g. `isinstance` instead of `type(...) is`)
- Exception classes: use relative imports to import base ValidationError
- Improve dynamic keyword argument construction in some classes like [here](https://github.com/binary-butterfly/validataclass/pull/120/commits/027ab1a9f273d43844308ae4f37f57dae8e6ffa1#diff-35059a70cee3bcbed31c465028dd5d37727743ac93a0e066e29cc72c58070ffcR61-R66)
- `FloatToDecimalValidator`: `min_value`/`max_value` will not be cast to string if specified as integer anymore (see [commit](https://github.com/binary-butterfly/validataclass/pull/120/commits/027ab1a9f273d43844308ae4f37f57dae8e6ffa1#diff-35942654b8e76bfaba18cfca910da74f8cb750a03ad76f5f778dc77bdf436be1R99-R100))
- `EnumValidator`: Improve type checking of Enum class when creating the validator (https://github.com/binary-butterfly/validataclass/pull/120/commits/9cbf9f9d1eab9f66d2fefd1b4d0a96e7b926e9c2)
- `DataclassValidator`: (https://github.com/binary-butterfly/validataclass/pull/120/commits/0b6ed6ebe0b37bc2c7125729684da57d5da10bf9)
  - Improve check if given dataclass is actually a dataclass
  - Simplified dataclass check and error handling: There is now only one error message for "parameter is a class that is not a dataclass" and "parameter is an instance of a dataclass".
  - Properly type `dataclasses.Field` with type parameter (requires extra code because in Python 3.8 it is not a Generic yet)
- Use parametrized `Pattern` type hints for regex patterns (uses `typing.Pattern` for now, because `re.Pattern` is not yet parametrized in Python 3.8) (https://github.com/binary-butterfly/validataclass/pull/120/commits/a0d0f546d58a9d22ccb7adcde6f3d5fea8e70893)
- `ValidataclassMixin`: Add a `# type: ignore[call-overload]` for the warning about `asdict` (https://github.com/binary-butterfly/validataclass/pull/120/commits/28110ce9da57958d694d210f31a73e0c5f549cca)
- Add `# type: ignore[override]` for `validate()` overrides in validator classes that return different types than their base validators (e.g. `DecimalValidator` returning `Decimal`, which is not compatible with `StringValidator` returning `str`) (https://github.com/binary-butterfly/validataclass/pull/120/commits/712ac98d798bb159e2ce7c4ec2d3fdbb08dd1fff)
- Add `# type: ignore` for sentinel object creation (https://github.com/binary-butterfly/validataclass/pull/120/commits/3965efe2d8ccb8df372ea05c25dae4afdff9c086)
  - There might be a better way to do this, but sentinels are weird, there should be language support for it.
- `unset_to_none()`: Use `isinstance(value, UnsetValueType)` instead of `is UnsetValue` (again, sentinels are weird) (https://github.com/binary-butterfly/validataclass/pull/120/commits/5ceea7434e1d50abfbd52102e9195529690becbd)
- Fix typing in `DateTimeRange` and `DateTimeOffsetRange` (https://github.com/binary-butterfly/validataclass/pull/120/commits/f7dd402be20a49d19f987a8a8fd387a163bc8f85)
- Various small changes to fix mypy issues
Support for mypy
This issue consists of two major parts: First, integrate mypy as a static type checker in the dev and build process of the library (and fix issues that mypy finds). Second, try to find out how to make validataclass **compatible** with mypy (i.e. in projects that use validataclass and mypy).

### Use mypy to check the library code

- [x] Add mypy to the dev environment (tox, Makefile) as an **optional** step for now.
  - Only add `/src` to the target files for now, but add a comment to add `/tests` later too.
  - (The tests will probably require the full validataclass-mypy compatibility.)
- [x] Fix all low-hanging fruit issues detected by mypy (some issues might need a bit more work)
- [x] Try to fix the more complicated issues (if necessary, mark them as `# type: ignore` for now, maybe create issues for them)
- [x] When all issues are resolved or ignored, make mypy a **required** (i.e. default) step in the Tox config.
- [x] Integrate mypy into the test and build CI pipelines
- [x] Fix typing issues in the unit tests that are **not** related to how typing in validataclasses works (don't permanently add `/tests` to the mypy config yet)

### Make validataclass fully mypy-compatible

- [x] Make package PEP 561 compatible: #125
- [ ] Run mypy on validataclass example code (e.g. the unit tests) and examine the issues.
  - Assigning validators to fields in validataclasses: `foo: int = IntegerValidator()` - it's a validator, not an int.
  - Subclasses of validataclasses with incompatible field type overrides (e.g. subclass changes field type from `int` to `OptionalUnset[int]`)
  - Sentinels: Checking `x is not UnsetValue` does not narrow down the type, and evaluating as boolean `if x:` doesn't either. (For the latter: would it help to declare the type of `UnsetValueType.__bool__` as `Literal[False]`?)
- [ ] Write mypy plugin.
- [ ] ???
- [ ] Tests and documentation
  - [ ] Document how to use mypy with validataclass (best practices etc.)
Support for mypy
This issue consists of two major parts: First, integrate mypy as a static type checker in the dev and build process of the library (and fix issues that mypy finds). Second, try to find out how to make validataclass **compatible** with mypy (i.e. in projects that use validataclass and mypy).

### Use mypy to check the library code

- [x] Add mypy to the dev environment (tox, Makefile) as an **optional** step for now.
  - Only add `/src` to the target files for now, but add a comment to add `/tests` later too.
  - (The tests will probably require the full validataclass-mypy compatibility.)
- [x] Fix all low-hanging fruit issues detected by mypy (some issues might need a bit more work)
- [x] Try to fix the more complicated issues (if necessary, mark them as `# type: ignore` for now, maybe create issues for them)
- [x] When all issues are resolved or ignored, make mypy a **required** (i.e. default) step in the Tox config.
- [x] Integrate mypy into the test and build CI pipelines
- [x] Fix typing issues in the unit tests that are **not** related to how typing in validataclasses works (don't permanently add `/tests` to the mypy config yet)

### Make validataclass fully mypy-compatible

- [x] Make package PEP 561 compatible: #125
- [ ] Run mypy on validataclass example code (e.g. the unit tests) and examine the issues.
  - Assigning validators to fields in validataclasses: `foo: int = IntegerValidator()` - it's a validator, not an int.
  - Subclasses of validataclasses with incompatible field type overrides (e.g. subclass changes field type from `int` to `OptionalUnset[int]`)
  - Sentinels: Checking `x is not UnsetValue` does not narrow down the type, and evaluating as boolean `if x:` doesn't either. (For the latter: would it help to declare the type of `UnsetValueType.__bool__` as `Literal[False]`?)
- [ ] Write mypy plugin.
- [ ] ???
- [ ] Tests and documentation
  - [ ] Document how to use mypy with validataclass (best practices etc.)
Support for mypy
This issue consists of two major parts: First, integrate mypy as a static type checker in the dev and build process of the library (and fix issues that mypy finds). Second, try to find out how to make validataclass **compatible** with mypy (i.e. in projects that use validataclass and mypy).

### Use mypy to check the library code

- [x] Add mypy to the dev environment (tox, Makefile) as an **optional** step for now.
  - Only add `/src` to the target files for now, but add a comment to add `/tests` later too.
  - (The tests will probably require the full validataclass-mypy compatibility.)
- [x] Fix all low-hanging fruit issues detected by mypy (some issues might need a bit more work)
- [x] Try to fix the more complicated issues (if necessary, mark them as `# type: ignore` for now, maybe create issues for them)
- [x] When all issues are resolved or ignored, make mypy a **required** (i.e. default) step in the Tox config.
- [x] Integrate mypy into the test and build CI pipelines
- [x] Fix typing issues in the unit tests that are **not** related to how typing in validataclasses works (don't permanently add `/tests` to the mypy config yet)

### Make validataclass fully mypy-compatible

- [x] Make package PEP 561 compatible: #125
- [ ] Run mypy on validataclass example code (e.g. the unit tests) and examine the issues.
  - Assigning validators to fields in validataclasses: `foo: int = IntegerValidator()` - it's a validator, not an int.
  - Subclasses of validataclasses with incompatible field type overrides (e.g. subclass changes field type from `int` to `OptionalUnset[int]`)
  - Sentinels: Checking `x is not UnsetValue` does not narrow down the type, and evaluating as boolean `if x:` doesn't either. (For the latter: would it help to declare the type of `UnsetValueType.__bool__` as `Literal[False]`?)
- [ ] Write mypy plugin.
- [ ] ???
- [ ] Tests and documentation
  - [ ] Document how to use mypy with validataclass (best practices etc.)


Discussion / hints:

Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index d483313..3951594 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,10 +11,23 @@ write_to = "src/validataclass/_version.py"
 version_scheme = "post-release"
 
 [tool.mypy]
-files = "src/"
+files = ["src/", "tests/"]
+mypy_path = "src/"
+explicit_package_bases = true
 
 # Enable strict type checking
 strict = true
 
 # Ignore errors like `Module "validataclass.exceptions" does not explicitly export attribute "..."`
 no_implicit_reexport = false
+
+[[tool.mypy.overrides]]
+module = 'tests.*'
+
+# Don't enforce typed definitions in tests, this is a lot of unnecessary work (most parameters would be Any anyway).
+allow_untyped_defs = true
+
+# TODO: This is the main issue with mypy and validataclass right now.
+# Defining dataclasses with validators using the @validataclass decorator, like `some_field: str = StringValidator()`,
+# will cause "Incompatible types in assignment" errors. Until we find a way to solve this, ignore this error for now.
+disable_error_code = "assignment"
diff --git a/setup.cfg b/setup.cfg
index 70668f8..ae8085e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -46,3 +46,4 @@ testing =
     coverage-conditional-plugin ~= 0.5
     flake8 ~= 7.0
     mypy ~= 1.9
+    types-python-dateutil
diff --git a/tox.ini b/tox.ini
index 90371bd..298ef83 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,8 +24,7 @@ deps = flake8
 commands = flake8 src/ tests/
 
 [testenv:mypy,py{312,311,310,39,38}-mypy]
-skip_install = true
-deps = mypy
+extras = testing
 commands = mypy
 
 [testenv:clean]


Test patch:
diff --git a/tests/dataclasses/_helpers.py b/tests/dataclasses/_helpers.py
index fe7f4be..7aa1e13 100644
--- a/tests/dataclasses/_helpers.py
+++ b/tests/dataclasses/_helpers.py
@@ -6,24 +6,35 @@
 
 import dataclasses
 import sys
-from typing import Any
+from typing import Any, Dict, Type, TYPE_CHECKING
 
 import pytest
 
 from validataclass.dataclasses import Default
+from validataclass.validators import T_Dataclass
+
+# TODO: Replace type alias with dataclasses.Field[Any] when removing Python 3.9 support. (#15)
+if TYPE_CHECKING:
+    T_DataclassField = dataclasses.Field[Any]
+else:
+    T_DataclassField = dataclasses.Field
 
 
 # Test helpers for dataclass tests
 
-def assert_field_default(field: dataclasses.Field, default_value: Any):
+def assert_field_default(field: T_DataclassField, default_value: Any) -> None:
     """
     Asserts that a given (vali-)dataclass field has a specified default value.
     """
-    # Check regular dataclass defaults
-    assert (
-        (field.default == default_value and field.default_factory is dataclasses.MISSING)
-        or (field.default is dataclasses.MISSING and field.default_factory() == default_value)
-    )
+    # Check that the field has a regular dataclass default VALUE or default FACTORY, but not both
+    assert field.default is not dataclasses.MISSING or field.default_factory is not dataclasses.MISSING
+    assert field.default is dataclasses.MISSING or field.default_factory is dataclasses.MISSING
+
+    # Check regular dataclass default
+    if field.default_factory is not dataclasses.MISSING:
+        assert field.default_factory() == default_value
+    else:
+        assert field.default == default_value
 
     # Check defaults in dataclass metadata
     metadata_default = field.metadata.get('validator_default')
@@ -31,7 +42,7 @@ def assert_field_default(field: dataclasses.Field, default_value: Any):
     assert metadata_default.get_value() == default_value
 
 
-def assert_field_no_default(field: dataclasses.Field):
+def assert_field_no_default(field: T_DataclassField) -> None:
     """
     Asserts that a given (vali-)dataclass field has no default value.
     """
@@ -43,15 +54,20 @@ def assert_field_no_default(field: dataclasses.Field):
 
     # For Python under 3.10, check that an exception raising default_factory is set
     if sys.version_info < (3, 10):
+        assert field.default_factory is not dataclasses.MISSING
         with pytest.raises(TypeError, match="required keyword-only argument"):
             field.default_factory()
     else:
         assert field.default_factory is dataclasses.MISSING
 
 
-def get_dataclass_fields(cls) -> dict:
+def get_dataclass_fields(cls: Type[T_Dataclass]) -> Dict[str, T_DataclassField]:
     """
     Returns a dictionary containing all fields of a given dataclass.
     """
+    # Make sure the class is really a dataclass
+    assert dataclasses.is_dataclass(cls) and isinstance(cls, type)
+
+    # Get fields and return them as a dictionary
     fields_tuple = dataclasses.fields(cls)
     return {field.name: field for field in fields_tuple}
diff --git a/tests/dataclasses/defaults_test.py b/tests/dataclasses/defaults_test.py
index 88c225b..0416994 100644
--- a/tests/dataclasses/defaults_test.py
+++ b/tests/dataclasses/defaults_test.py
@@ -5,6 +5,7 @@
 """
 
 from copy import copy
+from typing import Any, List
 
 import pytest
 
@@ -42,7 +43,7 @@ def test_default_immutable_values(value, expected_repr):
     @staticmethod
     def test_default_list_deepcopied():
         """ Test Default object with a list, make sure that it is deepcopied. """
-        default_list = []
+        default_list: List[Any] = []
         default = Default(default_list)
 
         # Check string representation and value
diff --git a/tests/dataclasses/validataclass_test.py b/tests/dataclasses/validataclass_test.py
index 6af6036..61aad5f 100644
--- a/tests/dataclasses/validataclass_test.py
+++ b/tests/dataclasses/validataclass_test.py
@@ -5,7 +5,7 @@
 """
 
 import dataclasses
-from typing import Dict, List, Optional
+from typing import Dict, List, Optional, Union
 
 import pytest
 
@@ -19,7 +19,7 @@
     validataclass_field,
 )
 from validataclass.exceptions import DataclassValidatorFieldException
-from validataclass.helpers import OptionalUnset, UnsetValue
+from validataclass.helpers import OptionalUnset, UnsetValue, UnsetValueType
 from validataclass.validators import (
     DictValidator,
     IntegerValidator,
@@ -279,7 +279,7 @@ class SubClass(BaseClass):
         # Check type annotations
         assert all(fields[field].type is int for field in ['required1', 'required2', 'optional2', 'optional4'])
         assert all(fields[field].type is Optional[int] for field in ['required3', 'optional1'])
-        assert all(fields[field].type is OptionalUnset[int] for field in ['required4', 'optional3'])
+        assert all(fields[field].type is Union[int, UnsetValueType] for field in ['required4', 'optional3'])
 
         # Check validators
         assert all(type(field.metadata.get('validator')) is IntegerValidator for field in fields.values())
@@ -391,7 +391,7 @@ class BaseB:
             field_both: str = StringValidator()
 
         @validataclass
-        class SubClass(BaseB, BaseA):
+        class SubClass(BaseB, BaseA):  # type: ignore[misc]
             # Override the defaults to test that the decorator recognizes all fields of both base classes.
             # If it does not, a "no validator for field X" error would be raised.
             field_a: int = Default(42)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 712ae31..e20fd8c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,12 +5,12 @@
 """
 
 from decimal import Decimal
-from typing import Any, List, Union
+from typing import Any, List, Tuple, Union
 
 from validataclass.validators import Validator
 
 
-def unpack_params(*args) -> List[tuple]:
+def unpack_params(*args: Any) -> List[Tuple[Any, ...]]:
     """
     Returns a list containing tuples build from the arguments.
 
@@ -64,7 +64,7 @@ def unpack_params(*args) -> List[tuple]:
     ]
     ```
     """
-    unpacked = [tuple()]
+    unpacked: List[Tuple[Any, ...]] = [tuple()]
 
     for arg in args:
         if type(arg) is list:
diff --git a/tests/validators/dataclass_validator_test.py b/tests/validators/dataclass_validator_test.py
index a337298..2409528 100644
--- a/tests/validators/dataclass_validator_test.py
+++ b/tests/validators/dataclass_validator_test.py
@@ -6,7 +6,7 @@
 
 from dataclasses import dataclass, field
 from decimal import Decimal
-from typing import Optional, List
+from typing import Any, Dict, List, Optional
 
 import pytest
 
@@ -94,7 +94,7 @@ class UnitTestPostValidationDataclass:
     start: int = IntegerValidator()
     end: int = IntegerValidator()
 
-    def __post_validate__(self):
+    def __post_validate__(self) -> None:
         if self.start > self.end:
             raise ValidationError(code='invalid_range', reason='"start" must be smaller than or equal to "end".')
 
@@ -110,7 +110,7 @@ class UnitTestContextSensitiveDataclass:
     name: str = UnitTestContextValidator()
     value: Optional[int] = IntegerValidator(), Default(None)
 
-    def __post_validate__(self, *, value_required: bool = False):
+    def __post_validate__(self, *, value_required: bool = False) -> None:
         if value_required and self.value is None:
             raise DataclassPostValidationError(field_errors={
                 'value': RequiredValueError(reason='Value is required in this context.'),
@@ -124,7 +124,7 @@ class UnitTestContextSensitiveDataclassWithPosArgs(UnitTestContextSensitiveDatac
     """
 
     # Same as UnitTestContextSensitiveDataclass, but with positional arguments
-    def __post_validate__(self, value_required: bool = False):
+    def __post_validate__(self, value_required: bool = False) -> None:
         super().__post_validate__(value_required=value_required)
 
 
@@ -149,7 +149,7 @@ class UnitTestContextSensitiveDataclassWithVarKwargs:
     ctx_b = None
     extra_kwargs = None
 
-    def __post_validate__(self, *, ctx_a: str = '', ctx_b: str = '', **kwargs):
+    def __post_validate__(self, *, ctx_a: str = '', ctx_b: str = '', **kwargs: Any) -> None:
         self.ctx_a = ctx_a
         self.ctx_b = ctx_b
         self.extra_kwargs = kwargs
@@ -166,7 +166,7 @@ class UnitTestPreValidateStaticMethodDataclass:
     example_int: int = IntegerValidator()
 
     @staticmethod
-    def __pre_validate__(input_data: dict) -> dict:
+    def __pre_validate__(input_data: Dict[Any, Any]) -> Dict[Any, Any]:
         mapping = {
             'exampleStr': 'example_str',
             'exampleInt': 'example_int',
@@ -193,7 +193,7 @@ class UnitTestPreValidateClassMethodDataclass:
     example_int: int = IntegerValidator()
 
     @classmethod
-    def __pre_validate__(cls, input_data: dict) -> dict:
+    def __pre_validate__(cls, input_data: Dict[Any, Any]) -> Dict[Any, Any]:
         for from_key, to_key in cls.__key_mapping.items():
             if from_key in input_data:
                 input_data[to_key] = input_data.pop(from_key)
@@ -212,7 +212,7 @@ class UnitTestPreValidateContextSensitiveDataclass:
     target_field: int = IntegerValidator()
 
     @classmethod
-    def __pre_validate__(cls, input_data: dict, *, source_field_name: str) -> dict:
+    def __pre_validate__(cls, input_data: Dict[Any, Any], *, source_field_name: str) -> Dict[Any, Any]:
         if source_field_name in input_data:
             return {'target_field': input_data[source_field_name]}
         else:
@@ -236,7 +236,7 @@ class UnitTestPreValidateContextSensitiveVarKwargsDataclass:
     example_int: int = IntegerValidator()
 
     @classmethod
-    def __pre_validate__(cls, input_data: dict, **kwargs) -> dict:
+    def __pre_validate__(cls, input_data: Dict[Any, Any], **kwargs: Any) -> Dict[Any, Any]:
         # Fill input_data with default values based on kwargs
         for key, default_value in kwargs.items():
             if key not in input_data:
@@ -252,7 +252,7 @@ class UnitTestInvalidPreValidateDataclass1:
     """ Dataclass with invalid __pre_validate__ class method: Not enough arguments. """
 
     @classmethod
-    def __pre_validate__(cls) -> dict:
+    def __pre_validate__(cls) -> Dict[Any, Any]:
         return {}
 
 
@@ -261,7 +261,7 @@ class UnitTestInvalidPreValidateDataclass2:
     """ Dataclass with invalid __pre_validate__ static method: Not enough arguments. """
 
     @staticmethod
-    def __pre_validate__() -> dict:
+    def __pre_validate__() -> Dict[Any, Any]:
         return {}
 
 
@@ -270,7 +270,7 @@ class UnitTestInvalidPreValidateDataclass3:
     """ Dataclass with invalid __pre_validate__ class method: Too many positional arguments. """
 
     @classmethod
-    def __pre_validate__(cls, input_data: dict, _extra_pos_argument) -> dict:
+    def __pre_validate__(cls, input_data: Dict[Any, Any], _extra_pos_argument: Any) -> Dict[Any, Any]:
         return input_data
 
 
@@ -279,7 +279,7 @@ class UnitTestInvalidPreValidateDataclass4:
     """ Dataclass with invalid __pre_validate__ static method: Too many positional arguments. """
 
     @staticmethod
-    def __pre_validate__(input_data: dict, _extra_pos_argument) -> dict:
+    def __pre_validate__(input_data: Dict[Any, Any], _extra_pos_argument: Any) -> Dict[Any, Any]:
         return input_data
 
 
@@ -288,7 +288,7 @@ class UnitTestInvalidPreValidateDataclass5:
     """ Dataclass with invalid __pre_validate__ class method: Too many (variable) positional arguments. """
 
     @classmethod
-    def __pre_validate__(cls, input_data: dict, *_args) -> dict:
+    def __pre_validate__(cls, input_data: Dict[Any, Any], *_args: Any) -> Dict[Any, Any]:
         return input_data
 
 
@@ -297,7 +297,7 @@ class UnitTestInvalidPreValidateDataclass6:
     """ Dataclass with invalid __pre_validate__ static method: Too many (variable) positional arguments. """
 
     @staticmethod
-    def __pre_validate__(input_data: dict, *_args) -> dict:
+    def __pre_validate__(input_data: Dict[Any, Any], *_args: Any) -> Dict[Any, Any]:
         return input_data
 
 
@@ -839,7 +839,8 @@ def test_dataclass_with_pre_validate_methods(
         dataclass_cls,
     ):
         """ Validate dataclasses with different __pre_validate__() methods (static and class methods). """
-        validator = DataclassValidator(dataclass_cls)
+        validator: DataclassValidator[Any] = DataclassValidator(dataclass_cls)
+
         validated_data = validator.validate(input_data)
 
         assert validated_data.example_str == expected_example_str
@@ -897,7 +898,7 @@ def test_dataclass_with_pre_validate_methods_invalid(
         dataclass_cls,
     ):
         """ Validate dataclasses with different __pre_validate__() methods and invalid input. """
-        validator = DataclassValidator(dataclass_cls)
+        validator: DataclassValidator[Any] = DataclassValidator(dataclass_cls)
 
         with pytest.raises(DictFieldsValidationError) as exception_info:
             validator.validate(input_data)
@@ -1048,7 +1049,7 @@ def test_dataclass_with_context_sensitive_pre_validate_with_var_kwargs_invalid(
     )
     def test_dataclass_with_invalid_forms_of_pre_validate(dataclass_cls):
         """ Test error handling for dataclasses with __pre_validate__() methods with an invalid method signature. """
-        validator = DataclassValidator(dataclass_cls)
+        validator: DataclassValidator[Any] = DataclassValidator(dataclass_cls)
 
         with pytest.raises(DataclassInvalidPreValidateSignatureException, match=PRE_VALIDATE_INVALID_SIGNATURE_ERROR):
             validator.validate({})
diff --git a/tests/validators/datetime_validator_test.py b/tests/validators/datetime_validator_test.py
index 7810ef9..6e5ad00 100644
--- a/tests/validators/datetime_validator_test.py
+++ b/tests/validators/datetime_validator_test.py
@@ -506,10 +506,14 @@ def test_with_local_timezone_valid(input_string, local_timezone, expected_dateti
         assert validated_dt == expected_datetime
 
         # Check timezone of datetimes by comparing their offset to UTC
-        assert (
-            validated_dt.tzinfo == expected_datetime.tzinfo
-            or validated_dt.tzinfo.utcoffset(validated_dt) == expected_datetime.tzinfo.utcoffset(expected_datetime)
-        )
+        if expected_datetime.tzinfo is None:
+            assert validated_dt.tzinfo is None
+        else:
+            assert validated_dt.tzinfo is not None
+            assert (
+                validated_dt.tzinfo == expected_datetime.tzinfo
+                or validated_dt.tzinfo.utcoffset(validated_dt) == expected_datetime.tzinfo.utcoffset(expected_datetime)
+            )
 
     # Test DateTimeValidator with target_timezone parameter
 
diff --git a/tests/validators/dict_validator_test.py b/tests/validators/dict_validator_test.py
index 739fd68..716ad06 100644
--- a/tests/validators/dict_validator_test.py
+++ b/tests/validators/dict_validator_test.py
@@ -563,7 +563,7 @@ class UnitTestDictValidator(DictValidator):
                 'value': DecimalValidator(),
                 'optional_value': DecimalValidator(),
             }
-            required_fields = ['name', 'value']
+            required_fields = {'name', 'value'}
 
         validator = UnitTestDictValidator()
         assert validator.validate(input_dict) == expected_output
@@ -608,7 +608,7 @@ class UnitTestDictValidator(DictValidator):
                 'value': DecimalValidator(),
                 'optional_value': DecimalValidator(),
             }
-            required_fields = ['name', 'value']
+            required_fields = {'name', 'value'}
 
         validator = UnitTestDictValidator()
 
diff --git a/tests/validators/discard_validator_test.py b/tests/validators/discard_validator_test.py
index 2b5a6c3..9949bf0 100644
--- a/tests/validators/discard_validator_test.py
+++ b/tests/validators/discard_validator_test.py
@@ -4,6 +4,8 @@
 Use of this source code is governed by an MIT-style license that can be found in the LICENSE file.
 """
 
+from typing import Any, List
+
 import pytest
 
 from validataclass.helpers import UnsetValue
@@ -15,7 +17,7 @@ class DiscardValidatorTest:
     Unit tests for the DiscardValidator.
     """
 
-    example_input_data = [
+    example_input_data: List[Any] = [
         None,
         True,
         False,
diff --git a/tests/validators/float_validator_test.py b/tests/validators/float_validator_test.py
index a8b05fa..3d423f7 100644
--- a/tests/validators/float_validator_test.py
+++ b/tests/validators/float_validator_test.py
@@ -4,6 +4,8 @@
 Use of this source code is governed by an MIT-style license that can be found in the LICENSE file.
 """
 
+from typing import Any, Dict
+
 import pytest
 
 from validataclass.exceptions import (
@@ -131,7 +133,7 @@ def test_float_value_range_invalid(min_value, max_value, input_data_list):
         validator = FloatValidator(min_value=min_value, max_value=max_value)
 
         # Construct error dict with min_value and/or max_value, depending on which is specified
-        expected_error_dict = {'code': 'number_range_error'}
+        expected_error_dict: Dict[str, Any] = {'code': 'number_range_error'}
         expected_error_dict.update({'min_value': float(min_value)} if min_value is not None else {})
         expected_error_dict.update({'max_value': float(max_value)} if max_value is not None else {})
 
diff --git a/tests/validators/list_validator_test.py b/tests/validators/list_validator_test.py
index 01ed1d8..4506c30 100644
--- a/tests/validators/list_validator_test.py
+++ b/tests/validators/list_validator_test.py
@@ -29,21 +29,21 @@ class ListValidatorTest:
     @staticmethod
     def test_valid_integer_list():
         """ Test ListValidator with IntegerValidator as item validator with valid integers. """
-        validator = ListValidator(item_validator=IntegerValidator())
+        validator: ListValidator[int] = ListValidator(item_validator=IntegerValidator())
 
         assert validator.validate([123, 0, -42, 123]) == [123, 0, -42, 123]
 
     @staticmethod
     def test_valid_integer_list_empty():
         """ Test ListValidator with IntegerValidator as item validator with an empty list. """
-        validator = ListValidator(item_validator=IntegerValidator())
+        validator: ListValidator[int] = ListValidator(item_validator=IntegerValidator())
 
         assert validator.validate([]) == []
 
     @staticmethod
     def test_valid_decimal_list():
         """ Test ListValidator with DecimalValidator as item validator with valid decimal strings. """
-        validator = ListValidator(item_validator=DecimalValidator())
+        validator: ListValidator[Decimal] = ListValidator(item_validator=DecimalValidator())
         output_list = validator.validate(['3.1415', '-0.42', '0'])
 
         assert all(isinstance(item, Decimal) for item in output_list)
@@ -86,7 +86,7 @@ def test_valid_decimal_list():
     )
     def test_valid_nested_list(input_list, expected_output):
         """ Test nested ListValidator to validate lists of lists of decimals. """
-        validator = ListValidator(ListValidator(DecimalValidator()))
+        validator: ListValidator[Decimal] = ListValidator(ListValidator(DecimalValidator()))
         output_list = validator.validate(input_list)
 
         for sublist in output_list:
@@ -100,7 +100,7 @@ def test_valid_nested_list(input_list, expected_output):
     @staticmethod
     def test_invalid_none():
         """ Check that ListValidator raises exceptions for None as value. """
-        validator = ListValidator(item_validator=IntegerValidator())
+        validator: ListValidator[int] = ListValidator(item_validator=IntegerValidator())
 
         with pytest.raises(RequiredValueError) as exception_info:
             validator.validate(None)
@@ -119,7 +119,7 @@ def test_invalid_none():
     )
     def test_invalid_not_a_list(input_data):
         """ Check that ListValidator raises exceptions for values that are not of type 'list'. """
-        validator = ListValidator(item_validator=StringValidator())
+        validator: ListValidator[str] = ListValidator(item_validator=StringValidator())
 
         with pytest.raises(InvalidTypeError) as exception_info:
             validator.validate(input_data)
@@ -132,7 +132,7 @@ def test_invalid_not_a_list(input_data):
     @staticmethod
     def test_invalid_decimal_list_items():
         """ Test ListValidator with DecimalValidator as item validator with invalid list items. """
-        validator = ListValidator(item_validator=DecimalValidator())
+        validator: ListValidator[Decimal] = ListValidator(item_validator=DecimalValidator())
 
         with pytest.raises(ListItemsValidationError) as exception_info:
             # Indices 1 and 4 are valid; indices 0, 2, 3 raise errors
@@ -158,7 +158,7 @@ def test_invalid_decimal_list_items():
     @staticmethod
     def test_with_context_arguments():
         """ Test that ListValidator passes context arguments down to the item validator. """
-        validator = ListValidator(item_validator=UnitTestContextValidator())
+        validator: ListValidator[str] = ListValidator(item_validator=UnitTestContextValidator())
 
         assert validator.validate(['unit', 'test']) == [
             "unit / {}",
@@ -201,7 +201,7 @@ def test_with_context_arguments():
     )
     def test_list_length_valid(min_length, max_length, input_data):
         """ Test ListValidator with length requirements with lists of the correct length. """
-        validator = ListValidator(
+        validator: ListValidator[int] = ListValidator(
             IntegerValidator(),
             min_length=min_length,
             max_length=max_length,
@@ -239,7 +239,7 @@ def test_list_length_valid(min_length, max_length, input_data):
     )
     def test_list_length_invalid(min_length, max_length, input_data):
         """ Test ListValidator with length requirements with lists of the wrong length. """
-        validator = ListValidator(
+        validator: ListValidator[int] = ListValidator(
             IntegerValidator(),
             min_length=min_length,
             max_length=max_length,
@@ -276,7 +276,7 @@ def test_list_length_invalid(min_length, max_length, input_data):
     )
     def test_discarding_invalid_items(input_data, expected_output):
         """ Test that ListValidator with discard_invalid=True discards invalid items. """
-        validator = ListValidator(
+        validator: ListValidator[int] = ListValidator(
             item_validator=IntegerValidator(),
             discard_invalid=True,
         )
@@ -305,7 +305,7 @@ def test_discard_invalid_with_length_requirements_valid(input_data, expected_out
         """
         Test that ListValidator with discard_invalid=True handles length requirements correctly, with valid input.
         """
-        validator = ListValidator(
+        validator: ListValidator[int] = ListValidator(
             IntegerValidator(),
             min_length=2,
             max_length=4,
@@ -339,7 +339,7 @@ def test_discard_invalid_with_length_requirements_invalid(input_data):
         Before item validation, minimum and maximum length must be checked. After item validation (and potential
         discarding), the minimum length needs to be checked again.
         """
-        validator = ListValidator(
+        validator: ListValidator[int] = ListValidator(
             IntegerValidator(),
             min_length=2,
             max_length=4,
diff --git a/tests/validators/regex_validator_test.py b/tests/validators/regex_validator_test.py
index d2d3d4b..7299616 100644
--- a/tests/validators/regex_validator_test.py
+++ b/tests/validators/regex_validator_test.py
@@ -438,7 +438,7 @@ def test_custom_error_class_invalid_type():
         Test that RegexValidator raises an error on init if the custom error class is not a ValidatonError subclass.
         """
         with pytest.raises(TypeError, match='Custom error class must be a subclass of ValidationError'):
-            RegexValidator('[0-9]', custom_error_class=Exception)  # noqa
+            RegexValidator('[0-9]', custom_error_class=Exception)  # type: ignore[arg-type]  # noqa
 
     # Tests with length requirements
 
diff --git a/tests/validators/reject_validator_test.py b/tests/validators/reject_validator_test.py
index d93a622..86cfad4 100644
--- a/tests/validators/reject_validator_test.py
+++ b/tests/validators/reject_validator_test.py
@@ -80,7 +80,7 @@ def test_reject_none(allow_none):
     def test_allow_none():
         """ Test that RejectValidator allows None if allow_none is True. """
         validator = RejectValidator(allow_none=True)
-        assert validator.validate(None) is None
+        assert validator.validate(None) is None  # type: ignore[func-returns-value]
 
     # Tests with custom errors
 
@@ -180,4 +180,4 @@ def test_custom_error_class_invalid_type():
         Test that RejectValidator raises an error on construction if the error class is not a ValidatonError subclass.
         """
         with pytest.raises(TypeError, match='Error class must be a subclass of ValidationError'):
-            RejectValidator(error_class=Exception)  # noqa
+            RejectValidator(error_class=Exception)  # type: ignore[arg-type]  # noqa
diff --git a/tests/validators/validator_test.py b/tests/validators/validator_test.py
index 775c229..b81ab07 100644
--- a/tests/validators/validator_test.py
+++ b/tests/validators/validator_test.py
@@ -24,7 +24,7 @@ def test_validate_without_kwargs_deprecation():
         # Ensure that Validator creation causes a DeprecationWarning
         with pytest.deprecated_call():
             class ValidatorWithoutKwargs(Validator):
-                def validate(self, input_data: Any) -> Any:  # noqa (missing parameter)
+                def validate(self, input_data: Any) -> Any:  # type: ignore[override]  # noqa
                     return input_data
 
         # Check that validate_with_context() calls validate() without errors