0
\$\begingroup\$

Greetings

One small problem that anyone have to tackle with in Python is handling invalid argument types ( without an automatic static type checking tool like Mypy )

One of the best methods for handling the invalid argument is raising an exception which would easily inform the code client with the required steps to fix, ( I.E ):

"Expected argument type passed for the parameter ( im_a_parameter ): ( int ) | Not: ( str )"


in this case, raising a ValueError would get the job done. While attending this method, as my project was growing larger, I found out that repeating this idiom would deny the DRY principle, so I rolled up my sleeves, and came up with a fixture:

unexpected_argument_type.py

"""
# The holder module for the UnexpectedArgumentTypeError exception class
"""



class UnexpectedArgumentTypeError(ValueError):
    def __init__(self, parameter: str, expected_argument_types: object | list[object]) -> None:
        """
        # Args:
            
            1. ( parameter ): The expected parameter of the current function signature.

            2. ( expected_argument_types ): The types that were expected to be passed to the function parameter.
        """

        self.passed_argument = parameter
        self.passed_argument_type = parameter
        self.expected_argument_types = expected_argument_types

        self._generate_error_message()
        super().__init__(self._error_message)

    def _generate_error_message(self):
        if isinstance(self.expected_argument_type, object) and not self._arguments_are_list:
            self._error_message = (
                f"Expected argument type passed for the parameter \
( {self.passed_argument} ): ( {self.expected_argument_type} ) | Not ( {self.passed_argument_type} )."
            )
            return 

        if isinstance(self.expected_argument_type, list):
            expected_argument_types_str = ', '.join(self.expected_argument_type)
            self._error_message = (
                f"Expected argument type passed for the parameter \
( {self.passed_argument} ): ( {expected_argument_types_str} ) | Not ( {self.passed_argument_type} )"
            )

    @property
    def passed_argument_type(self):
        return self._passed_argument_type
    
    @passed_argument_type.setter
    def passed_argument_type(self, argument_type):
        self._passed_argument_type = type(argument_type).__name__

    @property
    def expected_argument_types(self):
        return self._expected_argument_type
    
    @expected_argument_types.setter
    def expected_argument_type(self, arguments):
        if not isinstance(arguments, (list, object)):
            raise ValueError("The ( expected_argument_type ) argument must be object| e.x: ( str, tuple... )")
        
        if isinstance(arguments, list):
            self._arguments_are_list = True
            arguments = [str(argument) for argument in arguments]

        if isinstance(arguments, object) and not self._arguments_are_list:
            arguments = arguments.__name__

        self._expected_argument_type = arguments

After all of this, one question remains:

Does handling this problem worth the complexity that this piece of code adds? ( Writing unit tests, maintaining documentations ).

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Missing a lot of code. What is in coding, coding/database, and coding/database/exceptions? How is this fixture used? This is currently incomplete, so off-topic until the additional code is added. \$\endgroup\$
    – AJNeufeld
    Feb 9 at 17:35
  • \$\begingroup\$ @AJNeufeld Fixed. \$\endgroup\$
    – KhodeNima
    Feb 9 at 17:50

1 Answer 1

2
\$\begingroup\$

missing review context

This submission of a utility class is off-topic, as it does not offer any examples of caller code that relies on the utility. We can't see how caller's life is made better by existence of the utility, there's no "with" vs "without" contrast offered.

comparison with the competition

mypy

... handling invalid argument types ( without an automatic static type checking tool like Mypy )

There exist reasons why a project may be reluctant to adopt mypy or may choose to reject such linters. But this submission does not mention any of those reasons, so we cannot evaluate its fitness for purpose. In short, we don't know the intended use case.

beartype

Sometimes I do want strict runtime checking. I have been pretty happy with @beartype decorators.

>>> from beartype import beartype
>>> 
>>> @beartype
... def increment(n: int) -> int:
...     return n + 1
... 
>>> 
>>> increment(2)
3
>>> increment(2.0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<@beartype(__main__.increment) at 0x1057c4360>", line 28, in increment
beartype.roar.BeartypeCallHintParamViolation: Function __main__.increment() parameter n=2.0 violates type hint <class 'int'>, as float 2.0 not instance of int.

This submission makes no attempt to explain how it offers a solution competitive with mypy or @beartype.

Is handling this problem worth the complexity that this piece of code adds?

No.

\$\endgroup\$
1
  • \$\begingroup\$ In rev 3 we see unreviewed code like module_paths.append(...). I literally could not read that, I didn't know what it meant, and went looking for a sys.module_paths which of course doesn't exist. Eventually I noticed the deceptive import alias that turned familiar sys.path into something new but not improved. Prefer to use the bash command $ env PYTHONPATH=.:database:database/exceptions python foo.py so sys.path is set up properly from the get go, even during interpreter initialization. // Occasionally we prefer to import inside a def, due to time expense or error. Not here. \$\endgroup\$
    – J_H
    Feb 9 at 19:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.