Pythonic way of writing a library function which accepts multiple types?
Question: If, as a simplified example, I am writing a library to help people model
populations I might have a class such as:

    
    
    class Population:
        def __init__(self, t0, initial, growth):
            self.t0 = t0,
            self.initial = initial
            self.growth = growth
    

where t0 is of type datetime. Now I want to provide a method to determine the
population at a given time, whether that be a datetime or a float containing
the number of seconds since t0. Further, it would be reasonable for the caller
to provide an array of such times (if so, I think it reasonable to assume they
will all be of the same type). There are at least two ways I can see to
accomplish this:

  1. Method for each type
    
        def at_raw(self, t):
        if not isinstance(t, collections.Iterable):
            t = numpy.array([t])
        return self.initial*numpy.exp(self.growth*t)
    def at_datetime(self, t):
        if not isinstance(t, collections.Iterable):
            t = [t]
        dt = numpy.array([(t1-self.t0).total_seconds() for t1 in t])
        return self.at_raw(dt)
    

  2. Universal method
    
        def at(self, t):
        if isinstance(t, datetime):
            t = (t-self.t0).total_seconds()
        if isinstance(t, collections.Iterable):
            if isinstance(t[0], datetime):
                t = [(t1-self.t0).total_seconds() for t1 in t]
        else:
            t = np.array([t])
        return self.initial*numpy.exp(self.growth*t)
    

Either would work, but I'm not sure which is more pythonic. I've seen some
suggestions that type checking indicates bad design which would suggest method
1 but as this is a library intended for others to use, method 2 would probably
be more useful.

Note that it is necessary to support times given as floats, even if only the
library itself uses this feature, for example I might implement a method which
root finds for stationary points in a more complicated model where the float
representation is clearly preferable. Thanks in advance for any suggestions or
advice.


Answer: I believe you can simply stick with the Python's Duck Typing Philosophy here

    
    
    def at(self, t):
        def get_arr(t):
            try: # Iterate over me
                return [get_arr(t1)[0] for t1 in t]
            except TypeError:
                #Opps am not Iterable
                pass
            try: # you can subtract datetime object
                return [(t-self.t0).total_seconds()]
            except TypeError:
                #Opps am not a datetime object
                pass
            # I am just a float
            return [t]
        self.initial*numpy.exp(self.growth*np.array(get_arr(t)))
    

Its important, how you order the cases

  1. Specific Cases should precede generic cases. 
    
        def foo(num):
        """Convert a string implementation to
           Python Object"""
        try: #First check if its an Integer
            return int(num)
        except ValueError:
            #Well not an Integer
            pass
        try: #Check if its a float
            return float(num)
        except ValueError:
            pass
        #Invalid Number
        raise TypeError("Invalid Number Specified")
    

  2. Default Case should be the terminating case

  3. If successive cases, are mutually exclusive, order them by likeliness. 
  4. Prepare for the Unexpected by raising Exception. After all `Errors should never pass silently.`

