Skip to content

Conversation

@binaryDiv
Copy link
Contributor

This PR improves the typing of validator classes a lot by changing the Validator base class to a Generic class Validator[T]. This came with a lot of necessary follow-up changes, so it's a rather large PR. Most of it are new tests, though.

This is one big step towards full mypy support (see also: #116).

Generic Validator class

The Validator base class is a Generic class now (Validator[T]), with a type parameter that specifies the type of the validator's output value, i.e. the return type of the validate() method.

All classes have been adjusted to properly set this type parameter and add their own type parameters if needed to allow for proper typing. For example, ListValidator[T] (which already was a Generic) inherits from Validator[list[T]] now, and Noneable[T_Wrapped, T_Default] (new type parameters) inherits from Validator[T_Wrapped | T_Default].

Composition instead of inheritance for extended validators

Several "extended" validators that are based on another validator change the return type had to be modified to use composition rather than inheritance (otherwise changing the return type violates the LSP). For example, the DecimalValidator used to be a subclass of StringValidator to first validate the input as a string without reinventing the StringValidator, and then try to parse the string as a decimal. However, Decimal is not a subclass of str, so the return type of DecimalValidator wasn't compatible with the return type of its super class. Now, DecimalValidator inherits directly from Validator[Decimal] and uses a separate StringValidator (stored in an attribute) for the string validation. This is a breaking change, although one that should not matter that much in actual projects unless they meddle with a validator's internals (which are technically public, hence it's a breaking change).

The following validators are affected by this change:

  • DataclassValidator: Used to inherit from DictValidator, now it inherits from Validator[T_Dataclass] and uses a separate DictValidator. (This is an implementation detail that can change at any point in the future.)
  • DateValidator, DateTimeValidator, TimeValidator: Used to inherit from StringValidator, now inherit from Validator[datetime.date] etc. and use a separate StringValidator for basic string validation.
  • DecimalValidator: Used to inherit from StringValidator, now inherits from Validator[Decimal].
  • EnumValidator: Used to inherit from AnyOfValidator, now inherits from Validator[T_Enum] and uses a separate AnyOfValidator.

It is recommended for users of the library to check and adjust their custom validators accordingly. Validators based on inheritance should still work the same way as before, but if the return type is changed in an incompatible way, the validator should be rewritten to use composition for its base validator.

Please note that extending a validator by inheritance is still perfectly fine as long as the return type is the same as (or a subclass of) the original return type of the super class. For example, the FloatToDecimalValidator still inherits from DecimalValidator because it still returns a Decimal, it just modifies/extends the validation behavior.

_ensure_type with None

A minor change in the Validator._ensure_type() method was also done: This function now accepts None if type(None) is included in the list of accepted types. This is a small extension of that function and not a breaking change. It allows to simplify some code (and typing) e.g. in the AnyOfValidator because it doesn't need to handle None as a special case anymore.

Deprecate importing and reusing TypeVars

Another small change is that the TypeVars used by the built-in validators (T_Dataclass, T_Enum and T_ListItem) have been removed from __all__, i.e. the modules don't export these TypeVars anymore. They can still be imported and used, but this is considered deprecated now and linters should complain about importing them. Instead of reusing these TypeVars, it's recommended to define your own TypeVars (or use the new type parameter syntax when using Python 3.12 or above).

This is only a deprecation, not a breaking change yet, but the compatibility imports in validataclass.validators will be removed in a future version.

Typing tests with pytest-mypy-plugins

In order to test all of these new changes and provide a consistent typing experience, we've added a second type of automated tests besides unit tests. There are now "typing tests" using the pytest plugin pytest-mypy-plugins. These can be found in tests/mypy/ and are defined using YAML files. They will be automatically run together with the unit tests as part of running pytest (you can use make test-typing if you specifically only want to run the typing tests).

@binaryDiv binaryDiv self-assigned this Aug 12, 2025
@binaryDiv binaryDiv added testing Related to testing (e.g. unit tests) breaking changes This issue will cause a breaking change (or deprecation warning). refactoring Code refactoring, clean up and other code maintenance work. labels Aug 12, 2025
@ninanomenon
Copy link

I did a first review of your changes. Pretty cool! I am looking forward to better typing support! :D (less red underlines in my IDE are always good.)

Before I approve this PR I would like to do a quick test in IDE. I think I will do that in the next few days.

@ninanomenon ninanomenon self-requested a review August 25, 2025 06:54
@binaryDiv binaryDiv changed the base branch from main to dev-mypy August 25, 2025 12:48
@binaryDiv
Copy link
Contributor Author

I've decided to create a "dev-mypy" branch for all changes related to #116, including this one, and changed the target branch of this PR.

Reason: While this PR is basically self-contained, it doesn't make much sense to include it in a release before the other mypy-related changes are done. Also, the changes should be tested on an actual real-life code base before release, which also makes more sense in combination with the upcoming changes.

@binaryDiv binaryDiv merged commit 95eaed5 into dev-mypy Aug 25, 2025
5 checks passed
@binaryDiv binaryDiv deleted the generic-based-typing branch August 25, 2025 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changes This issue will cause a breaking change (or deprecation warning). refactoring Code refactoring, clean up and other code maintenance work. testing Related to testing (e.g. unit tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants