I like how the first one uses groupby() but I don't like the definition of "Monotonic" inside of the module, nor the name, nor returning a list instead of a generator. I also prefer functions instead of callable instances.

One alternative to use a function closure instead of a class:

import math
import itertools

def compare_with_previous():
prev = -math.inf
def compare(value):
nonlocal prev
test = prev < value
prev = value
return test
return compare

def monotonic(seq):
for is_increasing, group in itertools.groupby(seq, compare_with_previous()):
group = list(group)
if not is_increasing:
group.reverse()
yield group

print(list(monotonic([1, 2, 3, 2, 1, 4, 5, 6, 7])))

Another is to be more itertools oriented:

import math
import itertools

# Given a pair (a, b), test if a < b
def lt_pair(pair):
return pair[0] < pair[1]

def monotonic(seq):
# iterate through a two-element window of the sequence, using -inf as the first term
seq1, seq2 = itertools.tee(seq)
window = zip(itertools.chain([-math.inf], seq1), seq2)

# group by increasing or decreasing (arbitrarily assume equal means increasing)
for is_increasing, group in itertools.groupby(window, lt_pair):
if not is_increasing:
# reverse the direction
group = list(group)[::-1]
yield [pair[1] for pair in group]

print(list(monotonic([1, 2, 3, 2, 1, 4, 5, 6, 7])))

NOTE! None of these solutions support monotonicity correctly for equal values!

>>> print(list(monotonic([1, 1, 2, 3, 3, 2, 1, 1])))
[[1], [1], [2, 3], [1, 1, 2, 3]]
>>> list(mono_runs_simpler([1, 1, 2, 3, 3, 2, 1, 1]))
[[1], [1], [2, 3], [1, 2, 3], [1]]

That should be (IMO):

[[1, 1, 2, 3, 3], [2, 1, 1]]

as the term "strictly monotonic" is for the always-increasing or always-decreasing.

Handling this case is much easier in the first of these two implementations:

import math
import itertools

def monotonic_comparison():
prev_value = -math.inf
prev_test = True
def compare(value):
nonlocal prev_value, prev_test
if prev_test:
# Testing for monotonic increasing
test = prev_value <= value
else:
# Testing for monotonic decreasing
test = prev_value < value
prev_value = value
prev_test = test
return test

return compare

def monotonic(seq):
for is_increasing, group in itertools.groupby(seq, monotonic_comparison()):
group = list(group)
if not is_increasing:
group.reverse()
yield group

HOWEVER! At this point it's a lot of work structured around using groupby(). It's more readable, as Ned Batchelder points out, to just write the code directly, without groupby() or nonlocal:

import math
def monotonic_direct(seq):
prev_value = -math.inf
prev_test = True
group = []
for value in seq:
if prev_test:
test = prev_value <= value
else:
test = prev_value < value

if test == prev_test:
group.append(value)
else:
if not prev_test:
group.reverse()
yield group
group = [value]
prev_value = value
prev_test = test

if group:
yield group

>>> print(list(monotonic_direct([1, 2, 3, 2, 1, 4, 5, 6, 7])))
[[1, 2, 3], [1, 2], [4, 5, 6, 7]]
>>> print(list(monotonic_direct([1, 1, 2, 3, 3, 2, 1, 1])))
[[1, 1, 2, 3, 3], [2, 1, 1]]

(I'm using -inf as a special sentinel, instead of special-casing the [0] element but I think that's a minor point.)

EDIT: I am wrong. The initial -inf means the first term is always assumed to be in an increasing subsequence, even if it is part of a decreasing subsequence:

>>> list(monotonic_direct([10, 9, 8]))
[[10], [9, 8]]

As such, the original reference "Monotonic" implementation is wrong, as well as Ned's version!

>>> list(mono_runs_simpler([10, 9, 8]))
[[10], [9, 8]]

It should be [[10, 9, 8]].