Check for the type of arguments

Steven D'Aprano steve+comp.lang.python at pearwood.info
Sat Aug 17 11:46:35 EDT 2013


On Sat, 17 Aug 2013 05:34:54 -0700, Fernando Saldanha wrote:

> I am new to Python, with some experience in Java, C++ and R.
> 
> Writing in other languages I usually check the type and values of
> function arguments. 

Surely not in Java and C++. Being static-typed languages, the compiler 
checks them for you.


> In the Python code examples I have seen this is rarely done.

Correct. Rarely, but not never.



> Questions:
> 
> 1) Is this because it would be "unpythonic" or just because the examples
> are not really production code?

Both.

Production code does tend to have a little more type-checking than basic 
examples or throw-away code, but not excessively so. But even so, Python 
has a philosophy of "duck-typing". In other words, if an object quacks 
like a duck, and swims like a duck, then it's close enough to a duck. The 
result is that Python code is nearly always naturally polymorphic with no 
extra effort, unless you deliberately break duck-typing by performing 
unnecessary type-checks.

For example, you might have a function that expects a list, and then 
iterates over the list:

# Obviously this is a toy example.
def example(L):
    for item in L:
        if item == 1: print("found one")


You might be tempted to insist on a list, and put in type-checking, but 
that goes against Python's philosophy. Your code above works perfectly 
with list subclasses, and objects which delegate to lists, and tuples, 
and arrays, and sets, and iterators, and even dicts. Why insist on an 
actual duck when all you really need is a bird that swims? Any iterable 
object will work, not just lists.

And if the caller passes a non-iterable object? You get a nice, clean 
exception which can optionally be caught, not a segfault, kernel panic, 
or BSOD. Really, the only difference is you get a runtime exception 
instead of a compile-time error. This is more flexible, and tends to lead 
to more rapid development, but with a higher reliance on test-driven 
development, and less opportunity for code correctness proofs. So you win 
a bit, and lose a bit, but in my opinion you win more than you lose.

A related concept in Python is that we prefer to "ask for forgiveness 
rather than permission". Since all error checking happens at runtime, it 
often doesn't matter exactly when the error occurs. So if you need to 
sort a list:

def example(L):
    L.sort()
    ...


rather than checking up-front that L has a sort method, or that L is an 
actual list, just try to sort it. If L is a tuple, say, you will get a 
runtime exception. If you can recover from the exception, do so:

def example(L):
    try:
        L.sort()
    except AttributeError:  # no sort method
        L = list(L)
        L.sort()
    ...

but usually you will just let the exception propagate up to the caller, 
who can either catch it, or treat it as a fatal error.


> 2) If I still want to check the type of my arguments, do I
> 
> a) use type() or is instance() to check for type?

Depends what you want to do, but 99.99% of the time you will use 
isinstance.

"type(obj) is list" checks that obj is an actual list. Subclasses of list 
will be rejected. This is nearly always NOT what you want.

"isinstance(obj, list)" is better, since it accepts list subclasses. But 
still, very restrictive -- what's wrong with other sequences?

import collections
isinstance(obj, collections.Sequence)

is better still, since it accepts anything that is sequence-like, but it 
will still reject objects which would have worked perfectly well, such as 
iterators.

> b) use assert (I guess not), raise a ValueError, or sys.exit()?

None of the above.

Do not use assert for testing arguments. Two reasons:

1) Assertions are *optional*. If you run Python with the -O (optimize) 
flag, asserts are ignored.

2) It's the wrong error. (Same with ValueError, or UnicodeDecodeError, or 
ZeroDivisionError, etc.) Assertions are for checking internal code logic. 
"The caller passed an int when I need a list" is not a failure of your 
internal code logic, it is the wrong type of argument. So the exception 
you raise should be TypeError.

When in doubt, see what Python built-ins do:

py> sorted(24)  # expect a list, or other sequence
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable


TypeError. Not AssertionError.

And certainly not sys.exit(). That's both annoying and unnecessary:

- It's annoying when trying to debug code. Instead of getting a nice 
traceback, which shows you exactly what went wrong, the program just 
exits. Often coders give pathetically feeble error messages like:

"An error occurred"

but even if the error message is useful, you still throw away all the 
other information in the traceback. Consequently, there's no hint as to 
*where* the error was, only *what* it was.

- And it's unnecessary. If your aim is to exit the program, *any* 
uncaught exception will exit the program.


> (I noticed that raising a ValueError does not stop execution when I am
> running the Interactive Interpreter under PTVS, which I find
> inconvenient, but it does stop execution when running the code
> non-interactively.)


What's PTVS?

Of course exceptions stop execution of the current code. What they don't 
do is kill the interactive interpreter. That would be foolish. The whole 
point of the interactive interpreter is that it is supposed to be 
interactive and keep going even if you make a mistake. You're not 
supposed to use it for running live production code.


-- 
Steven



More information about the Python-list mailing list