tags:

views:

339

answers:

9

Python is one of my favorite languages, but I really have a love/hate relationship with it's dynamicness. Apart from the advantages, it often results in me forgetting to check a type, trying to call an attribute and getting the NoneType (or any other) has no attribute x error. A lot of them are pretty harmless but if not handled correctly they can bring down your entire app/process/etc. Over time I got better predicting where these could pop up and adding explicit type checking, but because I'm only human I miss one occasionally and then some end-user finds it.

So I'm interested in your strategy to avoid these. Do you use type-checking decorators? Maybe special object wrappers? Please share...

+4  A: 

If you write good unit tests for all of your code, you should find the errors very quickly when testing code.

Noctis Skytower
In my experience that's true for most of them, but not say the last 1% because (for us) it doesn't make sense (or is even possible) to unit test _every_ possible scenario.
Koen Bok
Testing becomes even more important in very dynamic languages, which have the potential for errors not even possible in more static languages.
Mike Graham
@Mike: on the bright side, writing unit test on dynamic languages is easier than in static languages.
voyager
A: 

I tend to use

if x is None:
    raise ValueError('x cannot be None')

But this will only work with the actual None value.

A more general approach is to test for the necessary attributes before you try to use them. For example:

def write_data(f):
    # Here we expect f is a file-like object.  But what if it's not?
    if not hasattr(f, 'write'):
        raise ValueError('write_data requires a file-like object')
    # Now we can do stuff with f that assumes it is a file-like object

The point of this code is that instead of getting an error message like "NoneType has no attribute write", you get "write_data requires a file-like object". The actual bug isn't in write_data(), and isn't really a problem with NoneType at all. The actual bug is in the code that calls write_data(). The key is to communicate that information as directly as possible.

Daniel Pryden
Your approach has little point to it. You're no better off raising your own error than having Python raise the one it would anyhow.
Mike Graham
Perhaps I didn't explain clearly enough. My point is exactly that: raise the error Python would raise anyway, just fail fast. As long as you always fail fast, you will be able find bugs (assuming your test coverage is reasonable). Anything more is really just trying to build a static type system on top of Python's dynamic one.
Daniel Pryden
what's wrong with `assert x is not None`?
gnibbler
@gnibbler: Asserts don't execute when you're running with optimizations enabled.
Daniel Pryden
@Daniel, I had the impression you were trying to pick these bugs up with your tests. That's what asserts are for, you shouldn't be running your tests with `-O`. As far as I know, `-O` doesn't do any other optimising than ignoring asserts.
gnibbler
@Daniel Pryden, You can only use code like this when you *know what to look for*, not when you have no way of expecting what the problem is. In these cases, you aren't really catching things earlier than you would otherwise (None will blow up as soon as you try to use it and `f.write` would fail right there.) Look-before-you-leap really does not prove to be a super-useful general strategy in Python. To make sure things are working right, your code should be well-documented, well-tested, well-reviewed and as simple as possible.
Mike Graham
`assert` isn't typically that useful because they are not guaranteed to run. This general topic is the case where they can be somewhat useful—for checking conditions that should *always* be true unless there is a bug in the coding (not just the usage), not in cases where you are sanitizing input or anything.
Mike Graham
@gnibbler: You make a valid point. But I understood the OP's question to be about cases where a bug slips past the tests ("I'm only human I miss one occasionally and then some end-user finds it"). My point was, rather than trying to use "explicit type checking", as the OP mentions, you really should be testing for *traits* of the types you expect. Sorry if that wasn't clear.
Daniel Pryden
@Mike Graham: You are of course correct. I'm not trying to say that documentation, testing, reviews, etc. are not important. I was trying to answer the specific question the OP raised, which is how to catch these kinds of bugs that slip through and show up to an end user. I agree that "look-before-you-leap really does not prove to be a super-useful general strategy in Python" -- but there are times when it is useful, and so I thought it was helpful to point out that this might be one of those times.
Daniel Pryden
@Daniel Pryden, It sounds like we interpreted the question a lot differently. It sounded to me like OP wanted a general strategy to deal with these sorts of bugs to use *every* time, not one particular time.
Mike Graham
A: 

I haven’t done a lot of Python programming, but I’ve done no programming at all in staticly typed languages, so I don’t tend to think about things in terms of variable types. That might explain why I haven’t come across this problem much. (Although the small amount of Python programming I’ve done might explain that too.)

I do enjoy Python 3’s revised handling of strings (i.e. all strings are unicode, everything else is just a stream of bytes), because in Python 2 you might not notice TypeErrors until dealing with unusual real world string values.

Paul D. Waite
Python 2 has two string types—`unicode` and `str`; the former is an abstract representation of text and the latter is a sequence of bytes. Python 3 renames `unicode` to `str` and makes some small changes and renames `str` to `bytes` and makes some medium changes. If you're using `unicode` right in Python 2, it should work almost exactly like Python 3.
Mike Graham
A: 

Something you can use to simplify your code is using the Null Object Design Pattern (to which I was introduced in Python Cookbook).

Roughly, the goal with Null objects is to provide an 'intelligent' replacement for the often used primitive data type None in Python or Null (or Null pointers) in other languages. These are used for many purposes including the important case where one member of some group of otherwise similar elements is special for whatever reason. Most often this results in conditional statements to distinguish between ordinary elements and the primitive Null value.

This object just eats the lack of attribute error, and you can avoid checking for their existence.

It's nothing more than

class Null(object):

    def __init__(self, *args, **kwargs):
        "Ignore parameters."
        return None

    def __call__(self, *args, **kwargs):
        "Ignore method calls."
        return self

    def __getattr__(self, mname):
        "Ignore attribute requests."
        return self

    def __setattr__(self, name, value):
        "Ignore attribute setting."
        return self

    def __delattr__(self, name):
        "Ignore deleting attributes."
        return self

    def __repr__(self):
        "Return a string representation."
        return "<Null>"

    def __str__(self):
        "Convert to a string and return it."
        return "Null"

With this, if you do Null("any", "params", "you", "want").attribute_that_doesnt_exists() it won't explode, but just silently become the equivalent of pass.

Normally you'd do something like

if obj.attr:
    obj.attr()

With this, you just do:

obj.attr()

and forget about it. Beware that extensive use of the Null object can potentially hide bugs in your code.

voyager
That seems quite dangerous to me. It goes against The Zen of Python: "Errors should never pass silently."
Daniel Pryden
But otherwise, the general idea of the Null Object design pattern is a good one. For example, don't use `None` to represent an empty iterable, use a real empty iterable like the empty list (or a custom class with similar effect).
Daniel Pryden
`None` is not a primitive data type. It is not a data type at all.
Mike Graham
Blowing up loudly and clearly as soon as something's wrong is usually a good thing. Sometimes you should propagate errors by repeatedly returning worthless values (such as `Nothing` when using the `Maybe` monad in Haskell or `NaN` when working with IEEE 754 floating points), but in Python the normal way of operating is to use exceptions to indicate errors.
Mike Graham
I'm aware that blowing up early is a feature, but this DP is still useful for example if you use it as `def fun(optional=Null()): return optional.attr`. This DP has a place, yet all the problems that you point out can be seen in http://www.python.org/dev/peps/pep-0336/. `Null` shouldn't replace `None` in every case.
voyager
That PEP doesn't really point out a problem. In fact, it concludes that making a null type that allows itself to be used falls short in tests for obviousness, clarity, explictness, and necessity.
Mike Graham
This "pattern" scares me.
FogleBird
+1  A: 

One advantage of TDD is that you end up writing code that is easier to write tests for.

Writing code first and then the tests can result in code that superficially works the same, but is much harder to write 100% coverage tests for.

Each case is likely to be different

It might make sense to have a decorator to check whether a particular parameter is None (or some other unexpected value) if you use it in a bunch of places.

Maybe it is appropriate to use the Null pattern - if the code is blowing up because you are setting the initial value to None, you could instead set the initial value to a null version of the object.

More and more wrappers can add up to quite a performance hit though, so it's always better to write code from the start that avoids the corner cases

gnibbler
+6  A: 

forgetting to check a type

This doesn't make much sense. You so rarely need to "check" a type. You simply run unit tests and if you've provided the wrong type object, things fail. You never need to "check" much, in my experience.

trying to call an attribute and getting the NoneType (or any other) has no attribute x error.

Unexpected None is a plain-old bug. 80% of the time, I omitted the return. Unit tests always reveal these.

Of those that remain, 80% of the time, they're plain old bugs due to an "early exit" which returns None because someone wrote an incomplete return statement. These if foo: return structures are easy to detect with unit tests. In some cases, they should have been if foo: return somethingMeaningful, and in still other cases, they should have been if foo: raise Exception("Foo").

The rest are dumb mistakes misreading the API's. Generally, mutator functions don't return anything. Sometimes I forget. Unit tests find these quickly, since basically, nothing works right.

That covers the "unexpected None" cases pretty solidly. Easy to unit test for. Most of the mistakes involve fairly trivial-to-write tests for some pretty obvious species of mistakes: wrong return; failure to raise an exception.

Other "has no attribute X" errors are really wild mistakes where a totally wrong type was used. That's either really wrong assignment statements or really wrong function (or method) calls. They always fail elaborately during unit testing, requiring very little effort to fix.

A lot of them are pretty harmless but if not handled correctly they can bring down your entire app/process/etc.

Um... Harmless? If it's a bug, I pray that it brings down my entire app as quickly as possible so I can find it. A bug that doesn't crash my app is the most horrible situation imaginable. "Harmless" isn't a word I'd use for a bug that fails to crash my app.

S.Lott
+1 for "A bug that doesn't crash my app is the most horrible situation imaginable."
Daniel Pryden
+1 Insightful, surely I meant harmless as "would be harmless if you dealt with them, which is often really easy".
Koen Bok
+1  A: 

One tool to try to help you keep your pieces fitting together well is interfaces. zope.interface is the most notable package in the Python world for using interfaces. Check out http://wiki.zope.org/zope3/WhatAreInterfaces and http://glyph.twistedmatrix.com/2009/02/explaining-why-interfaces-are-great.html to start to get an idea how interfaces and z.i in particular work. Interfaces can prove very useful in a large Python codebases.

Interfaces are no substitute for testing. Reasonably comprehensive testing is especially important in highly dynamic languages like Python where there are types of bugs that could not exist in a statically types language. Tests will also help you catch the sorts of bugs that are not unique to dynamic languages. Fortunately, developing in Python means that testing is easy (due to the flexibility) and you have plenty of time to write them that you saved because you're using Python.

Mike Graham
+2  A: 

You can also use decorators to enforce the type of attributes.

>>> @accepts(int, int, int)
... @returns(float)
... def average(x, y, z):
...     return (x + y + z) / 2
...
>>> average(5.5, 10, 15.0)
TypeWarning:  'average' method accepts (int, int, int), but was given
(float, int, float)
15.25
>>> average(5, 10, 15)
TypeWarning:  'average' method returns (float), but result is (int)
15

I'm not really a fan of them, but I can see their usefulness.

voyager
This is not a good strategy to make code that does not have the pitfalls OP fears.
Mike Graham
+1  A: 

forgetting to check a type

With duck typing, it shouldn't be necessary to check a type. But that's theory, in reality you will often want to validate input parameters (e.g. checking a UUID with a regex). For that purpose, I created myself some handy decorators for simple type and return type checking which are called like this:

@decorators.params(0, int, 2, str) # first parameter must be integer / third a string
@decorators.returnsOrNone(int, long) # must return an int/long value or None
def doSomething(integerParam, noMatterWhatParam, stringParam):
    ...

For everything else I mostly use assertions. Of course one often forgets to check a parameter, so it's necessary to test and to test often.

trying to call an attribute

Happens to me very seldom. Actually I often use methods instead of direct access to attributes (the "good" old getter/setter approach sometimes).

because I'm only human I miss one occasionally and then some end-user finds it

"Software is always completed at the customers'." - An anti-pattern which you should solve with unit tests that handle all possible cases in a function. Easier said than done, but it helps...

As for other common Python mistakes (mistyped names, wrong imports, ...), I'm using Eclipse with PyDev for projects (not for small scripts). PyDev warns you about most of the simple kinds of mistakes.

AndiDog
Checking that a UUID really is—or otherwise validating data—isn't typechecking *per se*, it is directly looking at what you're caring about. It is very often a good thing. Actual typechecking for a literal type (like your decorators do) is quite different and makes your code less flexible without anything really to gain most of the time.
Mike Graham
Using methods instead of direct access to another attribute does not help anything. A method still *is* an attribute, and one that you could misspell or use wrong. Using getters and setters, though, makes you have to write more boilerplate and clutter your API for nothing to gain. (In languages like C++, there is something to gain: you would have to change your API if you ever needed to change from a member to a method. In Python, you can use properties and therefore not have to change your API, so there's really no obvious reason to use getters and setters.)
Mike Graham
@Mike Graham: Correcto. That's why often duck typing is enough, like for iterators you would (almost) never do `assert isinstance(param, tuple)`. But if you define strict interfaces (like abstract base classes), it's sometimes a good idea to do strict type checking.
AndiDog
You would never do `assert` if it was a meaningful part of your code. Duck typing isn't somehow *less* than typechecking and only occasionally enough; it is a much more powerful, polymorphic strategy. Additionally, since this is still dynamic, it doesn't really help OP develop a strategy to catch problems early the way that testing and interfaces do.
Mike Graham
I only use assertions in code which can only be called internally. For other code, one should use `if` statements because assertions can be optimized away.
AndiDog