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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With