from itertools import zip_longest
from itertools import islice  # For slicing iterator objects


def grouped(iterable, num=2, *, ret_all=False, fill=False, fillvalue=None):
    """Group an iterable into pairs (or groups of n length).

    For example, list(grouped(range(20),3)) == [(0, 1, 2), (3, 4, 5), (6, 7, 8),
    (9, 10, 11), (12, 13, 14), (15, 16, 17)]

    Pass the ret_all flag to include the partial un-full group at the end, if
    one exists.
    Pass the fill flag with an optional fill value to fill the partial group at
    the end, if there is one, with the provided fill value (or none if no fill
    value was provided).
    """
    iterator = iter(iterable)  # Turn the iterable into an iterator
    if fill:  # If filling missing values
        # Import zip_longest to fill in missing values
        # Create N copies of the iterator, then zip them together, filling
        # missing values with the fill value
        return zip_longest(*[iterator] * num, fillvalue=fillvalue)
    # 
    if ret_all:  # If returning all elements
        def gen():  # Create a generator function, for similar functionality
            # Take the first n items and convert to a tuple
            out = tuple(islice(iterator, num))
            while out:  # While the next tuple is not empty
                yield out  # Yield the next tuple
                # Take the next n items and convert to a tuple
                out = tuple(islice(iterator, num))
            # 
        # 
        return gen()  # Return the generator function
    # 
    # If not filling and not returning all elements, create N copies of the
    # iterator and zip them, stopping at the shortest one
    return zip(*[iterator] * num)