2

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.

1
  • 1
    Off-topic, but copy would be better written as a class method, as it is an alternate constructor for your class. @classmethod def copy(cls, q): return Tee(Q.x). This requires a slight generalization of __init__ to def __init__(self, x=0): self.x = x. Commented Oct 10, 2018 at 13:40

2 Answers 2

5

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.

Sign up to request clarification or add additional context in comments.

1 Comment

FWIW, I'm using type hints virtually everywhere, but am just relying on my IDE (PyCharm) to highlight problems. That eases development a lot, since it finds certain classes of errors as I type them, but still has no influence on the runtime.
1

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.

6 Comments

I was with you there until the "not there to help developers … logic errors". Type hints and a proper tool that tells you about them can very well prevent certain types of logic errors which would otherwise just surface at runtime. Things like you forgetting that you're dealing with a list of things instead of just one thing and the like. These are also kinds of logic errors which proper type hints catch quickly.
I'm a C and C++ guy that is flirting with Python. Python OOP has important paradigm differences that I still need to understand. Your answer gave me a lot to think about, I appreciate it. Do you know some reference where I could improve my skills in this novel Python OOP paradigm, other than documentation?
@deceze those errors you're mentionning will 99 times out of 100 be caught vey quickly by your tests (unittests I mean), and sorry but (with due respect) your example is dubious at best - good naming conventions will immediatly make clear whether you're working on a collection or single object. I used to be a fervent proponant of static typing and when I started with Python I wasted a LOT of time trying to forcefit typechecking everywhere. Then I started reading some serious, real-life Python programs code and found out what I was doing was not only useless but also counter-productive.
Yes, plurals vs. singular should be a giveaway. But honestly, a lot of real-life Python programs are IMO too vague about their types and leave you guessing quite a lot about what values they will accept or expect. I find it faster to type hint and let my IDE help me than to (unit) test it to figure it out.
Type hints are indeed very useful as a documentation tool and I won't dispute this - and I also agree that some libs could be better documented but I never bothered contributing so I'd rather shut the f.. up xD. My main point is that all that brainwashing about static typing being the alpha and omega of code correctness and robustness is mostly bullshit.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.