iperetta
iperetta

Reputation: 617

Python OOP, type verification for arguments within methods / classes

I'm trying to understand OOP in Python and I have this "non-pythonic way of thinking" issue. I want a method for my class that verifies the type of the argument and raises an exception if it isn't of the proper type (e.g. ValueError). The closest to my desires that I got is this:

class Tee(object):
    def __init__(self):
        self.x = 0
    def copy(self, Q : '__main__.Tee'):
        self.x = Q.x
    def __str__(self):
        return str(self.x)

a = Tee()
b = Tee()
print(type(a))  # <class '__main__.Tee'>
print(isinstance(a, Tee))  # True
b.x = 255
a.copy(b)
print(a)        # 255
a.copy('abc')   # Traceback (most recent call last): [...]
                # AttributeError: 'str' object has no attribute 'x'

So, even that I tried to ensure the type of the argument Q in my copy method to be of the same class, the interpreter just passes through it and raises an AttributeError when it tries to get a x member out of a string.

I understand that I could do something like this:

[...]
    def copy(self, Q):
        if isinstance(Q, Tee):
            self.x = Q.x
        else:
            raise ValueError("Trying to copy from a non-Tee object")
[...]
a = Tee()
a.copy('abc')   # Traceback (most recent call last): [...]
                # ValueError: Trying to copy from a non-Tee object

But it sounds like a lot of work to implement everywhere around classes, even if I make a dedicated function, method or decorator. So, my question is: is there a more "pythonic" approach to this?

I'm using Python 3.6.5, by the way.

Upvotes: 2

Views: 180

Answers (2)

bruno desthuilliers
bruno desthuilliers

Reputation: 77892

So, my question is: is there a more "pythonic" approach to this?

Yes: clearly document what API is expected from the Q object (in this case: it should have an x int attribute) and call it a day.

The point is that whether you "validate" the argument's type or not, the error will happen at runtime, so from a practical POV typechecking or not won't make a huge difference - but it will prevent passing a "compatible" object for no good reason.

Also since Tee.x is public, it can be set to anything at any point in the code, and this is actually much more of a concern, since it can break at totally unrelated places, making the bug much more difficult to trace and solve, so if you really insist on being defensive (which may or not make sense depending on the context), that's what you should really focus on.

class Tee(object):
    def __init__(self):
        self.x = 0

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        # this will raise if value cannot be 
        # used to build an int
        self._x = int(value)


    def copy(self, Q : '__main__.Tee'):
        # we don't care what `Q` is as long
        # as it has an `x` attribute that can
        # be used for our purpose
        self.x = Q.x

    def __str__(self):
        return str(self.x)

This will 1/ prevent Tee.x from being unusable, and 2/ break at the exact point where an invalid value is passed, making the bug obvious and easy to fix by inspecting the traceback.

Note that point here is to say that typecheking is completely and definitely useless, but that (in Python at least) you should only use it when and where it really makes sense for the context. I know this might seems weird when you bought the idea that "static typing is good because it prevents errors" (been here, done that...), but actually type errors are rather rare (compared to logical errors) and most often quickly spotted. The truth about static typing is that it's not here to help the developer writing better code but to help the compiler optimizing code - which is a valuable goal but a totally different one.

Upvotes: 1

deceze
deceze

Reputation: 522032

Type annotations are not enforced at runtime. Period. They're currently only used by IDEs or static analysers like mypy, or by any code you write yourself that introspects these annotations. But since Python is largely based on duck typing, the runtime won't and doesn't actually enforce types.

This is usually good enough if you employ a static type checker during development to catch such errors. If you want to make actual runtime checks, you could use assertions:

assert isinstance(Q, Tee), f'Expected instance of Tee, got {type(Q)}'

But they are also mostly for debugging, since assertions can be turned off. To have strong type assertions, you need to be explicit:

if not isinstance(Q, Tee):
    raise TypeError(f'Expected instance of Tee, got {type(Q)}')

But again, this prevents duck typing, which isn't always desirable.

BTW, your type annotation should be just def copy(self, Q: 'Tee'), don't include '__main__'; also see https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563.

Upvotes: 5

Related Questions