views:

109

answers:

3

I'm still fairly new to Python and I'm trying to get used to its dynamic typing. Sometimes I have a function or a class that expects a parameter of a certain type, but could get a value of another type that's coercible to it. For example, it might expect a float but instead receive an int or a decimal. Or it might expect a string, but instead receive an object that defines the __str__ special method.

What is the best practice for coercing the argument to the right type (and the reason for it)? Do I do it in the function/class or in the caller? If in the caller, do I also check for it in the function? Eg.

Alternative 1:

def myfunc(takes_float):
    myval = float(takes_float)

myfunc(5)

Alternative 2:

def myfunc(takes_float):
    myval = takes_float

myfunc(float(5))

Alternative 3:

def myfunc(takes_float):
    assert isinstance(takes_float, float)
    myval = takes_float

myfunc(float(5))

I've already read this answer and this one and they say that checking types in Python is "bad", but I don't want to waste time tracking down very simple bugs which would be instantly picked up by the compiler in a statically typed language.

+8  A: 

You "coerce" (perhaps -- it could be a noop) when it's indispensable for you to do so, and no earlier. For example, say you have a function that takes a float and returns the sum of its sine and cosine:

import math
def spc(x):
  math.sin(x) + math.cos(x)

Where should you "coerce" x to float? Answer: nowhere at all -- sin and cos do that job for you, e.g.:

>>> spc(decimal.Decimal('1.9'))
0.62301052082391117

So when it is indispensable to coerce (as late as possible)? For example, if you want to call string methods on an argument, you do have to make sure it's a string -- trying to call e.g. .lower on a non-string won't work, len might work but do something different than you expect if the arg is e.g. a list (give you the number of items in the list, not the number of characters its representation as a string will take up), and so forth.

As for catching errors -- think unit tests -- semidecent unit tests will catch all errors static typing would, and then some. But, that's a different subject.

Alex Martelli
s/stating/static/
Andrey Vlasovskikh
@Andrey, yeah, it sounds better -- let me fix the A, thanks.
Alex Martelli
+2  A: 

It really depends. Why do you need a float? Would an int break the function? If so, why?

If you need the parameter to support a function/property that a float has but an int does not you should check for that function/property, not that the parameter happens to be a float. Check that the object can do what you need it to do, not that it happens to be a particular type that you're familiar with.

Who knows, maybe someone will find some major problem with Python's implementation of float and create a notbrokenfloat library. It might support everything a float does while fixing some exotic bug, but its objects wouldn't be of type float. Manually casting it to a float might remove all the benefits of this nifty new class (or could break outright).

Yes, that's an unlikely example, but I think that's the right mindset to get into when working with a dynamically typed language.

Steve Losh
A: 

There is exactly one time when integer vs. float will be a problem. This is the only time when you will find a "simple" bug that's weird and a challenge to debug.

Division.

Everything else does the conversion you need when you need it.

If you are using Python 2.x and casually throwing around / operators without thinking, you can -- under some common circumstances -- wind up doing the wrong thing.

You have several choices.

  1. from __future__ import division will give you Python 3 semantics for division.

  2. Run with the -Qnew option at all times to get the new division semantics.

  3. Use float near / operations.

Division is the only place where type can matter. It's the only time that integers behave differently from floats in a way that silently affects your results.

All other type mismatch problems will fail spectacularly with a TypeError exception. All others. You won't waste time debugging. You'll know immediately what's wrong.


To be more specific.

There's no debugging of "expect a string but didn't get a string". This will crash immediately with a traceback. No confusion. No time lost thinking. If a function expects a string, then the caller must provide the string -- that's the rule.

Alternative 2 above is used RARELY to correct the problem where you have a function that expects a string AND you got confused and forgot to provide a string. This mistake happens RARELY and it leads to an immediate type exception.

S.Lott
Thanks, good to know the options for that - but the int/float thing was just an example. There are other instances of the problem.
Evgeny
@Evgeny: Really? Can you provide any? The only one that's confusing is int/float. AFAIK, All others die with obvious exceptions. Please provide an example of an operation that doesn't die with an exception.
S.Lott
Another example is in my original post: the method might expect a string, but instead receive an object that defines the `__str__` special method. Somewhere along the line I need to call `str()` on it, but where?
Evgeny