The general case is to throw exceptions for exceptional circumstances. I wish that I could remember the exact quote (or who said it), but you should strive for functions that accept as many values and types as is reasonable and maintain a very narrowly defined behavior. This is a variant of what Nadia was talking about. Consider the following usages of your function:
intersect_two_lists(None, None)
intersect_two_lists([], ())
intersect_two_lists('12', '23')
intersect_two_lists([1, 2], {1: 'one', 2: 'two'})
intersect_two_lists(False, [1])
intersect_two_lists(None, [1])
I would expect that (5) throws an exception since passing False
is a type error. The rest of them, however, make some sort of sense but it really depends on the contract that the function states. If intersect_two_lists
were defined as returning the intersection of two iterables, then everything other than (5) should work as long as you make None
a valid representation of the empty set. The implementation would be something like:
def intersect_two_lists(seq1, seq2):
if seq1 is None: seq1 = []
if seq2 is None: seq2 = []
if not isinstance(seq1, collections.Iterable):
raise TypeError("seq1 is not Iterable")
if not isinstance(seq2, collections.Iterable):
raise TypeError("seq1 is not Iterable")
return filter(...)
I usually write helper functions that enforce whatever the contract is and then call them to check all of the preconditions. Something like:
def require_iterable(name, arg):
"""Returns an iterable representation of arg or raises an exception."""
if arg is not None:
if not isinstance(arg, collections.Iterable):
raise TypeError(name + " is not Iterable")
return arg
return []
def intersect_two_lists(seq1, seq2):
list1 = require_iterable("seq1", seq1)
list2 = require_iterable("seq2", seq2)
return filter(...)
You can also extend this concept and pass in the "policy" as an optional argument. I would not advise doing this unless you want to embrace Policy Based Design. I did want to mention it just in case you haven't looked into this option before.
If the contract for intersect_two_lists
is that it only accepts two non-empty list
parameters, then be explicit and throw exceptions if the contract is breached:
def require_non_empty_list(name, var):
if not isinstance(var, list):
raise TypeError(name + " is not a list")
if var == []:
raise ValueError(name + " is empty")
def intersect_two_lists(list1, list2):
require_non_empty_list('list1', list1)
require_non_empty_list('list2', list2)
return filter(...)
I think that the moral of the story is whatever you do, do it consistently and be explicit. Personally, I usually favor raising exceptions whenever a contract is breached or I am given a value that I really cannot use. If the values that I am given are reasonable, then I try to do something reasonable in return. You might also want to read the C++ FAQ Lite entry on exceptions. This particular entry gives you some more food for thought about exceptions.