# Buggy code
def func(self, other):
    if is_list_like(other) and len(other) != len(self):
        # TODO: Could this fail if the categories are listlike objects?
        raise ValueError("Lengths must match.")

    if not self.ordered:
        if opname in ["__lt__", "__gt__", "__le__", "__ge__"]:
            raise TypeError("Unordered Categoricals can only compare equality or not")
    if isinstance(other, Categorical):
        # Two Categoricals can only be be compared if the categories are
        # the same (maybe up to ordering, depending on ordered)

        msg = "Categoricals can only be compared if 'categories' are the same."
        if not self.is_dtype_equal(other):
            raise TypeError(msg)

        if not self.ordered and not self.categories.equals(other.categories):
            # both unordered and different order
            other_codes = _get_codes_for_values(other, self.categories)
        else:
            other_codes = other._codes

        ret = op(self._codes, other_codes)
        mask = (self._codes == -1) | (other_codes == -1)
        if mask.any():
            ret[mask] = fill_value
        return ret

    if is_scalar(other):
        if other in self.categories:
            i = self._unbox_scalar(other)
            ret = op(self._codes, i)

            if opname not in {"__eq__", "__ge__", "__gt__"}:
                # GH#29820 performance trick; get_loc will always give i>=0,
                #  so in the cases (__ne__, __le__, __lt__) the setting
                #  here is a no-op, so can be skipped.
                mask = self._codes == -1
                ret[mask] = fill_value
            return ret
        else:
            return ops.invalid_comparison(self, other, op)
    else:
        # allow categorical vs object dtype array comparisons for equality
        # these are only positional comparisons
        if opname not in ["__eq__", "__ne__"]:
            raise TypeError(
                f"Cannot compare a Categorical for op {opname} with "
                f"type {type(other)}.\nIf you want to compare values, "
                "use 'np.asarray(cat) <op> other'."
            )

        if isinstance(other, ExtensionArray) and needs_i8_conversion(other.dtype):
            # We would return NotImplemented here, but that messes up
            #  ExtensionIndex's wrapped methods
            return op(other, self)
        return getattr(np.array(self), opname)(np.array(other))


# Traceback (ValueError)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-4d3f2e8a3cfd> in <module>()
----> 1 c[0] = (1, 2)

/Users/taugspurger/miniconda3/envs/pandas-0.19.2/lib/python3.5/site-packages/pandas/core/categorical.py in __setitem__(self, key, value)
1652         # something to np.nan
1653         if len(to_add) and not isnull(to_add).all():
-> 1654             raise ValueError("Cannot setitem on a Categorical with a new "
1655                              "category, set the categories first")
1656

ValueError: Cannot setitem on a Categorical with a new category, set the categories first

# Fixed code
def func(self, other):
    hashable = is_hashable(other)
    if is_list_like(other) and len(other) != len(self) and not hashable:
        # in hashable case we may have a tuple that is itself a category
        raise ValueError("Lengths must match.")

    if not self.ordered:
        if opname in ["__lt__", "__gt__", "__le__", "__ge__"]:
            raise TypeError("Unordered Categoricals can only compare equality or not")
    if isinstance(other, Categorical):
        # Two Categoricals can only be be compared if the categories are
        # the same (maybe up to ordering, depending on ordered)

        msg = "Categoricals can only be compared if 'categories' are the same."
        if not self.is_dtype_equal(other):
            raise TypeError(msg)

        if not self.ordered and not self.categories.equals(other.categories):
            # both unordered and different order
            other_codes = _get_codes_for_values(other, self.categories)
        else:
            other_codes = other._codes

        ret = op(self._codes, other_codes)
        mask = (self._codes == -1) | (other_codes == -1)
        if mask.any():
            ret[mask] = fill_value
        return ret

    if hashable:
        if other in self.categories:
            i = self._unbox_scalar(other)
            ret = op(self._codes, i)

            if opname not in {"__eq__", "__ge__", "__gt__"}:
                # GH#29820 performance trick; get_loc will always give i>=0,
                #  so in the cases (__ne__, __le__, __lt__) the setting
                #  here is a no-op, so can be skipped.
                mask = self._codes == -1
                ret[mask] = fill_value
            return ret
        else:
            return ops.invalid_comparison(self, other, op)
    else:
        # allow categorical vs object dtype array comparisons for equality
        # these are only positional comparisons
        if opname not in ["__eq__", "__ne__"]:
            raise TypeError(
                f"Cannot compare a Categorical for op {opname} with "
                f"type {type(other)}.\nIf you want to compare values, "
                "use 'np.asarray(cat) <op> other'."
            )

        if isinstance(other, ExtensionArray) and needs_i8_conversion(other.dtype):
            # We would return NotImplemented here, but that messes up
            #  ExtensionIndex's wrapped methods
            return op(other, self)
        return getattr(np.array(self), opname)(np.array(other))