Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pythonic way of class argument validation

I would like to get some tips on the Pythonic way to validate the arguments when creating an instance of a class. I have a hard time understanding the proper usage of the __new__ method and maybe this is one of its usages? Say for example that i have class Test that takes in two arguments a and b. If, for example, i want to ensure that both must be integers and that b must be greater than a, i could do as follows:

class Test:
    def __init__(self, a, b):
        if not (isinstance(a,int) and isinstance(b,int)):
            raise Exception("bla bla error 1")
        if not b > a:
            raise Exception("bla bla error 2")

        self.a = a
        self.b = b
        #.....

or, i could do as follows:

def validate_test_input(a,b):
    if not (isinstance(a, int) and isinstance(b, int)):
        raise Exception("bla bla error 1")
    if not b > a:
        raise Exception("bla bla error 2")

class Test:
    def __init__(self, a, b):
        validate_test_input(a,b)
        self.a = a
        self.b = b
        #.....

What would you do? Is there any convention on data validation ? Should dunder new method be used here? If , so please show an example.

like image 218
JustANoob Avatar asked Aug 04 '26 21:08

JustANoob


2 Answers

First snippet is almost perfectly fine. Unless this logic is reused in several places in your code base I would avoid the second snippet because it decouples the logic from the class.

I would just do some small semantic changes.

  • Raise proper exception types, ie TypeError and ValueError

  • Rephrase the conditions to be more readable (you may disagree as this is quite subjective)

  • Of course provide a useful text instead of "bla bla", but I trust you with that one ;)

    class Test:
        def __init__(self, a, b):
            if not isinstance(a, int) or not isinstance(b, int):
                raise TypeError("bla bla error 1")
            if a <= b:
                raise ValueError("bla bla error 2")
    
            self.a = a
            self.b = b
            #.....
    

Some may find the original if not (isinstance(a, int) and isinstance(b, int)) to be more readable than what I suggested and I will not disagree. Same goes for if a <= b:. It depends if you prefer to stress the condition you want to be true or the condition you want to be false.

In this case, since we are raising an exception I prefer to stress the condition we want to be false.

like image 101
DeepSpace Avatar answered Aug 06 '26 10:08

DeepSpace


If this code is at development, I would maybe do that, which is not very different from your code:

class Test:
    def __init__(self, a, b):
        assert isinstance(a,int) and isinstance(b,int), "bla bla error 1"
        assert b > a, "bla bla error 2"

        self.a = a
        self.b = b
        #.....

And if I need this control when I will release that code (for example, if it is a library) I would convert asserts to raise, then raise a TypeError and a ValueError:

class Test:
    def __init__(self, a, b):
        if not (isinstance(a,int) and isinstance(b,int)):
            raise TypeError("bla bla error 1")
        if not b > a:
            raise ValueError("bla bla error 2")

        self.a = a
        self.b = b
        #.....

So your code is the true way to go.

In the case of __new__ magic method, today I found a good example in builtin turtle library. In the definition of Vec2D class:

class Vec2D(tuple):
    """A 2 dimensional vector class, used as a helper class
    for implementing turtle graphics.
    May be useful for turtle graphics programs also.
    Derived from tuple, so a vector is a tuple!

    Provides (for a, b vectors, k number):
       a+b vector addition
       a-b vector subtraction
       a*b inner product
       k*a and a*k multiplication with scalar
       |a| absolute value of a
       a.rotate(angle) rotation
    """
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))

    ...

As you know, tuple takes an argument which is iterable. Developers of this module probably wanted to change it, so they defined __new__ as (cls, x, y), and then they called tuple.__new__ as (cls, (x, y)). The cls in here is the class which is instanced. For more information, look at here: Calling __new__ when making a subclass of tuple

like image 23
Ekrem Dinçel Avatar answered Aug 06 '26 11:08

Ekrem Dinçel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!