Compare length of iterator with as few `next` calls as possible. How can I deduplicate this code?
To know the answer to your comparison to \$N\$, you need to attempt at least:


\$N\$ next calls if you test with \$\lt\$ (succeeding will result to False) or \$\ge\$ (succeeding will result to True);
\$N+1\$ next calls in all 4 other cases.


Obviously, if the iterator length is fewer than \$N\$ there will be less calls.

So, in order to simplify things, you could extract the first \$N+1\$ elements of the iterator and count them to compare this count to \$N\$:


extracting can be simplified with itertools.islice
counting can be done using either


enumerate and dumping the content into a 1-length collections.deque

from itertools import islice
from collections import deque


def compare_length(iterable, n, operation):
    chunck = islice(iterable, max(0, n+1))
    try:
        (length, _) = deque(enumerate(chunk, 1), maxlen=1)
    except ValueError:
        length = 0
    return operation(length, n)

sum

from itertools import islice


def compare_length(iterable, n, operation):
    chunck = islice(iterable, max(0, n+1))
    length = sum(1 for _ in chunck)
    return operation(length, n)




The former should be more efficient, the latter more readable. As suggested in the question, this function takes the operation to perform as its third argument and compares min(&lt;iterable length&gt;, n+1) to n. This is sufficient in most cases.



If you truly want to reduce the amount of next calls needed for \$\lt\$ and \$\ge\$, you can keep your class approach and factorize the min(&lt;iterable length&gt;, n) part into an helper method:

from itertools import islice


class ILengthComparator:
    def __init__(self, iterable):
        self._iterable = iterable

    def _length_up_to(self, n):
        return sum(1 for _ in islice(self._iterable, max(0, n)))

    def __eq__(self, n):
        return self._length_up_to(n+1) == n

    def __ne__(self, n):
        return self._length_up_to(n+1) != n

    def __lt__(self, n):
        return self._length_up_to(n) &lt; n

    def __le__(self, n):
        return self._length_up_to(n+1) &lt;= n

    def __gt__(self, n):
        return self._length_up_to(n+1) &gt; n

    def __ge__(self, n):
        return self._length_up_to(n) &gt;= n




Lastly, if you want to avoid consuming the iterator while performing those tests, you can check itertools.tee.