Group values in the *src* iterable by the value returned by *key*,
    which defaults to :class:`bool`, grouping values by truthiness.

    >>> bucketize(range(5))
    {False: [0], True: [1, 2, 3, 4]}
    >>> is_odd = lambda x: x % 2 == 1
    >>> bucketize(range(5), is_odd)
    {False: [0, 2, 4], True: [1, 3]}

    Value lists are not deduplicated:

    >>> bucketize([None, None, None, 'hello'])
    {False: [None, None, None], True: ['hello']}

    Bucketize into more than 3 groups

    >>> bucketize(range(10), lambda x: x % 3)
    {0: [0, 3, 6, 9], 1: [1, 4, 7], 2: [2, 5, 8]}

    ``bucketize`` has a couple of advanced options useful in certain
    cases.  *value_transform* can be used to modify values as they are
    added to buckets, and *key_filter* will allow excluding certain
    buckets from being collected.

    >>> bucketize(range(5), value_transform=lambda x: x*x)
    {False: [0], True: [1, 4, 9, 16]}

    >>> bucketize(range(10), key=lambda x: x % 3, key_filter=lambda k: k % 3 != 1)
    {0: [0, 3, 6, 9], 2: [2, 5, 8]}

    Note in some of these examples there were at most two keys, ``True`` and
    ``False``, and each key present has a list with at least one
    item. See :func:`partition` for a version specialized for binary
    use cases.

def bucketize(src, key=None, value_transform=None, key_filter=None):
    """Group values in the *src* iterable by the value returned by *key*,
    which defaults to :class:`bool`, grouping values by truthiness.

    >>> bucketize(range(5))
    {False: [0], True: [1, 2, 3, 4]}
    >>> is_odd = lambda x: x % 2 == 1
    >>> bucketize(range(5), is_odd)
    {False: [0, 2, 4], True: [1, 3]}

    Value lists are not deduplicated:

    >>> bucketize([None, None, None, 'hello'])
    {False: [None, None, None], True: ['hello']}

    Bucketize into more than 3 groups

    >>> bucketize(range(10), lambda x: x % 3)
    {0: [0, 3, 6, 9], 1: [1, 4, 7], 2: [2, 5, 8]}

    ``bucketize`` has a couple of advanced options useful in certain
    cases.  *value_transform* can be used to modify values as they are
    added to buckets, and *key_filter* will allow excluding certain
    buckets from being collected.

    >>> bucketize(range(5), value_transform=lambda x: x*x)
    {False: [0], True: [1, 4, 9, 16]}

    >>> bucketize(range(10), key=lambda x: x % 3, key_filter=lambda k: k % 3 != 1)
    {0: [0, 3, 6, 9], 2: [2, 5, 8]}

    Note in some of these examples there were at most two keys, ``True`` and
    ``False``, and each key present has a list with at least one
    item. See :func:`partition` for a version specialized for binary
    use cases.

    """
    if not is_iterable(src):
        raise TypeError('expected an iterable')
    if key is None:
        key = bool
    if not callable(key):
        raise TypeError('expected callable key function')
    if value_transform is None:
        value_transform = lambda x: x
    if not callable(value_transform):
        raise TypeError('expected callable value transform function')

    ret = {}
    for val in src:
        key_of_val = key(val)
        if key_filter is None or key_filter(key_of_val):
            ret.setdefault(key_of_val, []).append(value_transform(val))
    return ret