text: from __future__ import annotations
  
  from datetime import datetime
  import functools
  from itertools import zip_longest
  import operator
  from typing import (
      TYPE_CHECKING,
      Any,
      Callable,
      Hashable,
      Literal,
      Sequence,
      TypeVar,
      cast,
      final,
      overload,
  )
  import warnings
  
  import numpy as np
  
  from pandas._config import get_option
  
  from pandas._libs import (
      algos as libalgos,
      index as libindex,
      lib,
  )
  import pandas._libs.join as libjoin
  from pandas._libs.lib import (
      is_datetime_array,
      no_default,
  )
  from pandas._libs.tslibs import (
      IncompatibleFrequency,
      NaTType,
      OutOfBoundsDatetime,
      Timestamp,
      tz_compare,
  )
  from pandas._typing import (
      AnyArrayLike,
      ArrayLike,
      Dtype,
      DtypeObj,
      F,
      Shape,
      npt,
  )
  from pandas.compat.numpy import function as nv
  from pandas.errors import (
      DuplicateLabelError,
      InvalidIndexError,
  )
  from pandas.util._decorators import (
      Appender,
      cache_readonly,
      deprecate_nonkeyword_arguments,
      doc,
  )
  from pandas.util._exceptions import (
      find_stack_level,
      rewrite_exception,
  )
  
  from pandas.core.dtypes.cast import (
      can_hold_element,
      find_common_type,
      infer_dtype_from,
      maybe_cast_pointwise_result,
      validate_numeric_casting,
  )
  from pandas.core.dtypes.common import (
      ensure_int64,
      ensure_object,
      ensure_platform_int,
      is_bool_dtype,
      is_categorical_dtype,
      is_dtype_equal,
      is_ea_or_datetimelike_dtype,
      is_extension_array_dtype,
      is_float,
      is_float_dtype,
      is_hashable,
      is_integer,
      is_interval_dtype,
      is_iterator,
      is_list_like,
      is_numeric_dtype,
      is_object_dtype,
      is_scalar,
      is_signed_integer_dtype,
      is_unsigned_integer_dtype,
      needs_i8_conversion,
      pandas_dtype,
      validate_all_hashable,
  )
  from pandas.core.dtypes.concat import concat_compat
  from pandas.core.dtypes.dtypes import (
      CategoricalDtype,
      DatetimeTZDtype,
      ExtensionDtype,
      IntervalDtype,
      PandasDtype,
      PeriodDtype,
  )
  from pandas.core.dtypes.generic import (
      ABCDataFrame,
      ABCDatetimeIndex,
      ABCMultiIndex,
      ABCPeriodIndex,
      ABCSeries,
      ABCTimedeltaIndex,
  )
  from pandas.core.dtypes.inference import is_dict_like
  from pandas.core.dtypes.missing import (
      array_equivalent,
      is_valid_na_for_dtype,
      isna,
  )
  
  from pandas.core import (
      arraylike,
      missing,
      ops,
  )
  from pandas.core.accessor import CachedAccessor
  import pandas.core.algorithms as algos
  from pandas.core.array_algos.putmask import (
      setitem_datetimelike_compat,
      validate_putmask,
  )
  from pandas.core.arrays import (
      Categorical,
      ExtensionArray,
  )
  from pandas.core.arrays.datetimes import (
      tz_to_dtype,
      validate_tz_from_dtype,
  )
  from pandas.core.arrays.sparse import SparseDtype
  from pandas.core.base import (
      IndexOpsMixin,
      PandasObject,
  )
  import pandas.core.common as com
  from pandas.core.construction import (
      ensure_wrapped_if_datetimelike,
      extract_array,
      sanitize_array,
  )
  from pandas.core.indexers import deprecate_ndim_indexing
  from pandas.core.indexes.frozen import FrozenList
  from pandas.core.ops import get_op_result_name
  from pandas.core.ops.invalid import make_invalid_op
  from pandas.core.sorting import (
      ensure_key_mapped,
      get_group_index_sorter,
      nargsort,
  )
  from pandas.core.strings import StringMethods
  
  from pandas.io.formats.printing import (
      PrettyDict,
      default_pprint,
      format_object_summary,
      pprint_thing,
  )
  
  if TYPE_CHECKING:
      from pandas import (
          CategoricalIndex,
          DataFrame,
          IntervalIndex,
          MultiIndex,
          Series,
      )
      from pandas.core.arrays import PeriodArray
  
  
  __all__ = ["Index"]
  
  _unsortable_types = frozenset(("mixed", "mixed-integer"))
  
  _index_doc_kwargs: dict[str, str] = {
      "klass": "Index",
      "inplace": "",
      "target_klass": "Index",
      "raises_section": "",
      "unique": "Index",
      "duplicated": "np.ndarray",
  }
  _index_shared_docs: dict[str, str] = {}
  str_t = str
  
  
  _o_dtype = np.dtype("object")
  
  
  def _maybe_return_indexers(meth: F) -> F:
      """
      Decorator to simplify 'return_indexers' checks in Index.join.
      """
  
      @functools.wraps(meth)
      def join(
          self,
          other,
          how: str_t = "left",
          level=None,
          return_indexers: bool = False,
          sort: bool = False,
      ):
          join_index, lidx, ridx = meth(self, other, how=how, level=level, sort=sort)
          if not return_indexers:
              return join_index
  
          if lidx is not None:
              lidx = ensure_platform_int(lidx)
          if ridx is not None:
              ridx = ensure_platform_int(ridx)
          return join_index, lidx, ridx
  
      return cast(F, join)
  
  
  def disallow_kwargs(kwargs: dict[str, Any]) -> None:
      if kwargs:
          raise TypeError(f"Unexpected keyword arguments {repr(set(kwargs))}")
  
  
  def _new_Index(cls, d):
      """
      This is called upon unpickling, rather than the default which doesn't
      have arguments and breaks __new__.
      """
      # required for backward compat, because PI can't be instantiated with
      # ordinals through __new__ GH #13277
      if issubclass(cls, ABCPeriodIndex):
          from pandas.core.indexes.period import _new_PeriodIndex
  
          return _new_PeriodIndex(cls, **d)
  
      if issubclass(cls, ABCMultiIndex):
          if "labels" in d and "codes" not in d:
              # GH#23752 "labels" kwarg has been replaced with "codes"
              d["codes"] = d.pop("labels")
  
      elif "dtype" not in d and "data" in d:
          # Prevent Index.__new__ from conducting inference;
          #  "data" key not in RangeIndex
          d["dtype"] = d["data"].dtype
      return cls.__new__(cls, **d)
  
  
  _IndexT = TypeVar("_IndexT", bound="Index")
  
  
  class Index(IndexOpsMixin, PandasObject):
      """
      Immutable sequence used for indexing and alignment. The basic object
      storing axis labels for all pandas objects.
  
      Parameters
      ----------
      data : array-like (1-dimensional)
      dtype : NumPy dtype (default: object)
          If dtype is None, we find the dtype that best fits the data.
          If an actual dtype is provided, we coerce to that dtype if it's safe.
          Otherwise, an error will be raised.
      copy : bool
          Make a copy of input ndarray.
      name : object
          Name to be stored in the index.
      tupleize_cols : bool (default: True)
          When True, attempt to create a MultiIndex if possible.
  
      See Also
      --------
      RangeIndex : Index implementing a monotonic integer range.
      CategoricalIndex : Index of :class:`Categorical` s.
      MultiIndex : A multi-level, or hierarchical Index.
      IntervalIndex : An Index of :class:`Interval` s.
      DatetimeIndex : Index of datetime64 data.
      TimedeltaIndex : Index of timedelta64 data.
      PeriodIndex : Index of Period data.
      NumericIndex : Index of numpy int/uint/float data.
      Int64Index : Index of purely int64 labels (deprecated).
      UInt64Index : Index of purely uint64 labels (deprecated).
      Float64Index : Index of  purely float64 labels (deprecated).
  
      Notes
      -----
      An Index instance can **only** contain hashable objects
  
      Examples
      --------
      >>> pd.Index([1, 2, 3])
      Int64Index([1, 2, 3], dtype='int64')
  
      >>> pd.Index(list('abc'))
      Index(['a', 'b', 'c'], dtype='object')
      """
  
      # tolist is not actually deprecated, just suppressed in the __dir__
      _hidden_attrs: frozenset[str] = (
          PandasObject._hidden_attrs
          | IndexOpsMixin._hidden_attrs
          | frozenset(["contains", "set_value"])
      )
  
      # To hand over control to subclasses
      _join_precedence = 1
  
      # Cython methods; see github.com/cython/cython/issues/2647
      #  for why we need to wrap these instead of making them class attributes
      # Moreover, cython will choose the appropriate-dtyped sub-function
      #  given the dtypes of the passed arguments
  
      @final
      def _left_indexer_unique(self: _IndexT, other: _IndexT) -> npt.NDArray[np.intp]:
          # Caller is responsible for ensuring other.dtype == self.dtype
          sv = self._get_engine_target()
          ov = other._get_engine_target()
          return libjoin.left_join_indexer_unique(sv, ov)
  
      @final
      def _left_indexer(
          self: _IndexT, other: _IndexT
      ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          # Caller is responsible for ensuring other.dtype == self.dtype
          sv = self._get_engine_target()
          ov = other._get_engine_target()
          joined_ndarray, lidx, ridx = libjoin.left_join_indexer(sv, ov)
          joined = self._from_join_target(joined_ndarray)
          return joined, lidx, ridx
  
      @final
      def _inner_indexer(
          self: _IndexT, other: _IndexT
      ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          # Caller is responsible for ensuring other.dtype == self.dtype
          sv = self._get_engine_target()
          ov = other._get_engine_target()
          joined_ndarray, lidx, ridx = libjoin.inner_join_indexer(sv, ov)
          joined = self._from_join_target(joined_ndarray)
          return joined, lidx, ridx
  
      @final
      def _outer_indexer(
          self: _IndexT, other: _IndexT
      ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          # Caller is responsible for ensuring other.dtype == self.dtype
          sv = self._get_engine_target()
          ov = other._get_engine_target()
          joined_ndarray, lidx, ridx = libjoin.outer_join_indexer(sv, ov)
          joined = self._from_join_target(joined_ndarray)
          return joined, lidx, ridx
  
      _typ: str = "index"
      _data: ExtensionArray | np.ndarray
      _data_cls: type[np.ndarray] | type[ExtensionArray] = np.ndarray
      _id: object | None = None
      _name: Hashable = None
      # MultiIndex.levels previously allowed setting the index name. We
      # don't allow this anymore, and raise if it happens rather than
      # failing silently.
      _no_setting_name: bool = False
      _comparables: list[str] = ["name"]
      _attributes: list[str] = ["name"]
      _is_numeric_dtype: bool = False
      _can_hold_na: bool = True
      _can_hold_strings: bool = True
  
      # Whether this index is a NumericIndex, but not a Int64Index, Float64Index,
      # UInt64Index or RangeIndex. Needed for backwards compat. Remove this attribute and
      # associated code in pandas 2.0.
      _is_backward_compat_public_numeric_index: bool = False
  
      _engine_type: type[libindex.IndexEngine] = libindex.ObjectEngine
      # whether we support partial string indexing. Overridden
      # in DatetimeIndex and PeriodIndex
      _supports_partial_string_indexing = False
  
      _accessors = {"str"}
  
      str = CachedAccessor("str", StringMethods)
  
      # --------------------------------------------------------------------
      # Constructors
  
      def __new__(
          cls, data=None, dtype=None, copy=False, name=None, tupleize_cols=True, **kwargs
      ) -> Index:
  
          if kwargs:
              warnings.warn(
                  "Passing keywords other than 'data', 'dtype', 'copy', 'name', "
                  "'tupleize_cols' is deprecated and will raise TypeError in a "
                  "future version.  Use the specific Index subclass directly instead.",
                  FutureWarning,
                  stacklevel=find_stack_level(),
              )
  
          from pandas.core.arrays import PandasArray
          from pandas.core.indexes.range import RangeIndex
  
          name = maybe_extract_name(name, data, cls)
  
          if dtype is not None:
              dtype = pandas_dtype(dtype)
          if "tz" in kwargs:
              tz = kwargs.pop("tz")
              validate_tz_from_dtype(dtype, tz)
              dtype = tz_to_dtype(tz)
  
          if isinstance(data, PandasArray):
              # ensure users don't accidentally put a PandasArray in an index.
              data = data.to_numpy()
          if isinstance(dtype, PandasDtype):
              dtype = dtype.numpy_dtype
  
          data_dtype = getattr(data, "dtype", None)
  
          # range
          if isinstance(data, (range, RangeIndex)):
              result = RangeIndex(start=data, copy=copy, name=name)
              if dtype is not None:
                  return result.astype(dtype, copy=False)
              return result
  
          elif is_ea_or_datetimelike_dtype(dtype):
              # non-EA dtype indexes have special casting logic, so we punt here
              klass = cls._dtype_to_subclass(dtype)
              if klass is not Index:
                  return klass(data, dtype=dtype, copy=copy, name=name, **kwargs)
  
              ea_cls = dtype.construct_array_type()
              data = ea_cls._from_sequence(data, dtype=dtype, copy=copy)
              data = np.asarray(data, dtype=object)
              disallow_kwargs(kwargs)
              return Index._simple_new(data, name=name)
  
          elif is_ea_or_datetimelike_dtype(data_dtype):
              data_dtype = cast(DtypeObj, data_dtype)
              klass = cls._dtype_to_subclass(data_dtype)
              if klass is not Index:
                  result = klass(data, copy=copy, name=name, **kwargs)
                  if dtype is not None:
                      return result.astype(dtype, copy=False)
                  return result
  
              data = np.array(data, dtype=object, copy=copy)
              disallow_kwargs(kwargs)
              return Index._simple_new(data, name=name)
  
          # index-like
          elif (
              isinstance(data, Index)
              and data._is_backward_compat_public_numeric_index
              and dtype is None
          ):
              return data._constructor(data, name=name, copy=copy)
          elif isinstance(data, (np.ndarray, Index, ABCSeries)):
  
              if isinstance(data, ABCMultiIndex):
                  data = data._values
  
              if dtype is not None:
                  # we need to avoid having numpy coerce
                  # things that look like ints/floats to ints unless
                  # they are actually ints, e.g. '0' and 0.0
                  # should not be coerced
                  # GH 11836
                  data = sanitize_array(data, None, dtype=dtype, copy=copy)
  
                  dtype = data.dtype
  
              if data.dtype.kind in ["i", "u", "f"]:
                  # maybe coerce to a sub-class
                  arr = data
              else:
                  arr = com.asarray_tuplesafe(data, dtype=np.dtype("object"))
  
                  if dtype is None:
                      arr = _maybe_cast_data_without_dtype(
                          arr, cast_numeric_deprecated=True
                      )
                      dtype = arr.dtype
  
                      if kwargs:
                          return cls(arr, dtype, copy=copy, name=name, **kwargs)
  
              klass = cls._dtype_to_subclass(arr.dtype)
              arr = klass._ensure_array(arr, dtype, copy)
              disallow_kwargs(kwargs)
              return klass._simple_new(arr, name)
  
          elif is_scalar(data):
              raise cls._scalar_data_error(data)
          elif hasattr(data, "__array__"):
              return Index(np.asarray(data), dtype=dtype, copy=copy, name=name, **kwargs)
          else:
  
              if tupleize_cols and is_list_like(data):
                  # GH21470: convert iterable to list before determining if empty
                  if is_iterator(data):
                      data = list(data)
  
                  if data and all(isinstance(e, tuple) for e in data):
                      # we must be all tuples, otherwise don't construct
                      # 10697
                      from pandas.core.indexes.multi import MultiIndex
  
                      return MultiIndex.from_tuples(
                          data, names=name or kwargs.get("names")
                      )
              # other iterable of some kind
  
              subarr = com.asarray_tuplesafe(data, dtype=np.dtype("object"))
              if dtype is None:
                  # with e.g. a list [1, 2, 3] casting to numeric is _not_ deprecated
                  # error: Incompatible types in assignment (expression has type
                  # "Union[ExtensionArray, ndarray[Any, Any]]", variable has type
                  # "ndarray[Any, Any]")
                  subarr = _maybe_cast_data_without_dtype(  # type: ignore[assignment]
                      subarr, cast_numeric_deprecated=False
                  )
                  dtype = subarr.dtype
              return Index(subarr, dtype=dtype, copy=copy, name=name, **kwargs)
  
      @classmethod
      def _ensure_array(cls, data, dtype, copy: bool):
          """
          Ensure we have a valid array to pass to _simple_new.
          """
          if data.ndim > 1:
              # GH#13601, GH#20285, GH#27125
              raise ValueError("Index data must be 1-dimensional")
          if copy:
              # asarray_tuplesafe does not always copy underlying data,
              #  so need to make sure that this happens
              data = data.copy()
          return data
  
      @final
      @classmethod
      def _dtype_to_subclass(cls, dtype: DtypeObj):
          # Delay import for perf. https://github.com/pandas-dev/pandas/pull/31423
  
          if isinstance(dtype, ExtensionDtype):
              if isinstance(dtype, DatetimeTZDtype):
                  from pandas import DatetimeIndex
  
                  return DatetimeIndex
              elif isinstance(dtype, CategoricalDtype):
                  from pandas import CategoricalIndex
  
                  return CategoricalIndex
              elif isinstance(dtype, IntervalDtype):
                  from pandas import IntervalIndex
  
                  return IntervalIndex
              elif isinstance(dtype, PeriodDtype):
                  from pandas import PeriodIndex
  
                  return PeriodIndex
  
              elif isinstance(dtype, SparseDtype):
                  return cls._dtype_to_subclass(dtype.subtype)
  
              return Index
  
          if dtype.kind == "M":
              from pandas import DatetimeIndex
  
              return DatetimeIndex
  
          elif dtype.kind == "m":
              from pandas import TimedeltaIndex
  
              return TimedeltaIndex
  
          elif is_float_dtype(dtype):
              from pandas.core.api import Float64Index
  
              return Float64Index
          elif is_unsigned_integer_dtype(dtype):
              from pandas.core.api import UInt64Index
  
              return UInt64Index
          elif is_signed_integer_dtype(dtype):
              from pandas.core.api import Int64Index
  
              return Int64Index
  
          # error: Non-overlapping equality check (left operand type: "dtype[Any]", right
          # operand type: "Type[object]")
          elif dtype == object:  # type: ignore[comparison-overlap]
              # NB: assuming away MultiIndex
              return Index
  
          elif issubclass(dtype.type, (str, bool, np.bool_)):
              return Index
  
          raise NotImplementedError(dtype)
  
      """
      NOTE for new Index creation:
  
      - _simple_new: It returns new Index with the same type as the caller.
        All metadata (such as name) must be provided by caller's responsibility.
        Using _shallow_copy is recommended because it fills these metadata
        otherwise specified.
  
      - _shallow_copy: It returns new Index with the same type (using
        _simple_new), but fills caller's metadata otherwise specified. Passed
        kwargs will overwrite corresponding metadata.
  
      See each method's docstring.
      """
  
      @property
      def asi8(self):
          """
          Integer representation of the values.
  
          Returns
          -------
          ndarray
              An ndarray with int64 dtype.
          """
          warnings.warn(
              "Index.asi8 is deprecated and will be removed in a future version.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          return None
  
      @classmethod
      def _simple_new(cls: type[_IndexT], values, name: Hashable = None) -> _IndexT:
          """
          We require that we have a dtype compat for the values. If we are passed
          a non-dtype compat, then coerce using the constructor.
  
          Must be careful not to recurse.
          """
          assert isinstance(values, cls._data_cls), type(values)
  
          result = object.__new__(cls)
          result._data = values
          result._name = name
          result._cache = {}
          result._reset_identity()
  
          return result
  
      @classmethod
      def _with_infer(cls, *args, **kwargs):
          """
          Constructor that uses the 1.0.x behavior inferring numeric dtypes
          for ndarray[object] inputs.
          """
          with warnings.catch_warnings():
              warnings.filterwarnings("ignore", ".*the Index constructor", FutureWarning)
              result = cls(*args, **kwargs)
  
          if result.dtype == object and not result._is_multi:
              # error: Argument 1 to "maybe_convert_objects" has incompatible type
              # "Union[ExtensionArray, ndarray[Any, Any]]"; expected
              # "ndarray[Any, Any]"
              values = lib.maybe_convert_objects(result._values)  # type: ignore[arg-type]
              if values.dtype.kind in ["i", "u", "f"]:
                  return Index(values, name=result.name)
  
          return result
  
      @cache_readonly
      def _constructor(self: _IndexT) -> type[_IndexT]:
          return type(self)
  
      @final
      def _maybe_check_unique(self) -> None:
          """
          Check that an Index has no duplicates.
  
          This is typically only called via
          `NDFrame.flags.allows_duplicate_labels.setter` when it's set to
          True (duplicates aren't allowed).
  
          Raises
          ------
          DuplicateLabelError
              When the index is not unique.
          """
          if not self.is_unique:
              msg = """Index has duplicates."""
              duplicates = self._format_duplicate_message()
              msg += f"\n{duplicates}"
  
              raise DuplicateLabelError(msg)
  
      @final
      def _format_duplicate_message(self) -> DataFrame:
          """
          Construct the DataFrame for a DuplicateLabelError.
  
          This returns a DataFrame indicating the labels and positions
          of duplicates in an index. This should only be called when it's
          already known that duplicates are present.
  
          Examples
          --------
          >>> idx = pd.Index(['a', 'b', 'a'])
          >>> idx._format_duplicate_message()
              positions
          label
          a        [0, 2]
          """
          from pandas import Series
  
          duplicates = self[self.duplicated(keep="first")].unique()
          assert len(duplicates)
  
          out = Series(np.arange(len(self))).groupby(self).agg(list)[duplicates]
          if self._is_multi:
              # test_format_duplicate_labels_message_multi
              # error: "Type[Index]" has no attribute "from_tuples"  [attr-defined]
              out.index = type(self).from_tuples(out.index)  # type: ignore[attr-defined]
  
          if self.nlevels == 1:
              out = out.rename_axis("label")
          return out.to_frame(name="positions")
  
      # --------------------------------------------------------------------
      # Index Internals Methods
  
      @final
      def _get_attributes_dict(self) -> dict[str_t, Any]:
          """
          Return an attributes dict for my class.
  
          Temporarily added back for compatibility issue in dask, see
          https://github.com/pandas-dev/pandas/pull/43895
          """
          warnings.warn(
              "The Index._get_attributes_dict method is deprecated, and will be "
              "removed in a future version",
              DeprecationWarning,
              stacklevel=find_stack_level(),
          )
          return {k: getattr(self, k, None) for k in self._attributes}
  
      def _shallow_copy(self: _IndexT, values, name: Hashable = no_default) -> _IndexT:
          """
          Create a new Index with the same class as the caller, don't copy the
          data, use the same object attributes with passed in attributes taking
          precedence.
  
          *this is an internal non-public method*
  
          Parameters
          ----------
          values : the values to create the new Index, optional
          name : Label, defaults to self.name
          """
          name = self._name if name is no_default else name
  
          return self._simple_new(values, name=name)
  
      def _view(self: _IndexT) -> _IndexT:
          """
          fastpath to make a shallow copy, i.e. new object with same data.
          """
          result = self._simple_new(self._values, name=self._name)
  
          result._cache = self._cache
          return result
  
      @final
      def _rename(self: _IndexT, name: Hashable) -> _IndexT:
          """
          fastpath for rename if new name is already validated.
          """
          result = self._view()
          result._name = name
          return result
  
      @final
      def is_(self, other) -> bool:
          """
          More flexible, faster check like ``is`` but that works through views.
  
          Note: this is *not* the same as ``Index.identical()``, which checks
          that metadata is also the same.
  
          Parameters
          ----------
          other : object
              Other object to compare against.
  
          Returns
          -------
          bool
              True if both have same underlying data, False otherwise.
  
          See Also
          --------
          Index.identical : Works like ``Index.is_`` but also checks metadata.
          """
          if self is other:
              return True
          elif not hasattr(other, "_id"):
              return False
          elif self._id is None or other._id is None:
              return False
          else:
              return self._id is other._id
  
      @final
      def _reset_identity(self) -> None:
          """
          Initializes or resets ``_id`` attribute with new object.
          """
          self._id = object()
  
      @final
      def _cleanup(self) -> None:
          self._engine.clear_mapping()
  
      @cache_readonly
      def _engine(self) -> libindex.IndexEngine:
          # For base class (object dtype) we get ObjectEngine
  
          # to avoid a reference cycle, bind `target_values` to a local variable, so
          # `self` is not passed into the lambda.
          target_values = self._get_engine_target()
          return self._engine_type(target_values)
  
      @final
      @cache_readonly
      def _dir_additions_for_owner(self) -> set[str_t]:
          """
          Add the string-like labels to the owner dataframe/series dir output.
  
          If this is a MultiIndex, it's first level values are used.
          """
          return {
              c
              for c in self.unique(level=0)[: get_option("display.max_dir_items")]
              if isinstance(c, str) and c.isidentifier()
          }
  
      # --------------------------------------------------------------------
      # Array-Like Methods
  
      # ndarray compat
      def __len__(self) -> int:
          """
          Return the length of the Index.
          """
          return len(self._data)
  
      def __array__(self, dtype=None) -> np.ndarray:
          """
          The array interface, return my values.
          """
          return np.asarray(self._data, dtype=dtype)
  
      def __array_ufunc__(self, ufunc: np.ufunc, method: str_t, *inputs, **kwargs):
          if any(isinstance(other, (ABCSeries, ABCDataFrame)) for other in inputs):
              return NotImplemented
  
          result = arraylike.maybe_dispatch_ufunc_to_dunder_op(
              self, ufunc, method, *inputs, **kwargs
          )
          if result is not NotImplemented:
              return result
  
          if "out" in kwargs:
              # e.g. test_dti_isub_tdi
              return arraylike.dispatch_ufunc_with_out(
                  self, ufunc, method, *inputs, **kwargs
              )
  
          if method == "reduce":
              result = arraylike.dispatch_reduction_ufunc(
                  self, ufunc, method, *inputs, **kwargs
              )
              if result is not NotImplemented:
                  return result
  
          new_inputs = [x if x is not self else x._values for x in inputs]
          result = getattr(ufunc, method)(*new_inputs, **kwargs)
          if ufunc.nout == 2:
              # i.e. np.divmod, np.modf, np.frexp
              return tuple(self.__array_wrap__(x) for x in result)
  
          return self.__array_wrap__(result)
  
      def __array_wrap__(self, result, context=None):
          """
          Gets called after a ufunc and other functions e.g. np.split.
          """
          result = lib.item_from_zerodim(result)
          if is_bool_dtype(result) or lib.is_scalar(result) or np.ndim(result) > 1:
              return result
  
          return Index(result, name=self.name)
  
      @cache_readonly
      def dtype(self) -> DtypeObj:
          """
          Return the dtype object of the underlying data.
          """
          return self._data.dtype
  
      @final
      def ravel(self, order="C"):
          """
          Return an ndarray of the flattened values of the underlying data.
  
          Returns
          -------
          numpy.ndarray
              Flattened array.
  
          See Also
          --------
          numpy.ndarray.ravel : Return a flattened array.
          """
          warnings.warn(
              "Index.ravel returning ndarray is deprecated; in a future version "
              "this will return a view on self.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          if needs_i8_conversion(self.dtype):
              # Item "ndarray[Any, Any]" of "Union[ExtensionArray, ndarray[Any, Any]]"
              # has no attribute "_ndarray"
              values = self._data._ndarray  # type: ignore[union-attr]
          elif is_interval_dtype(self.dtype):
              values = np.asarray(self._data)
          else:
              values = self._get_engine_target()
          return values.ravel(order=order)
  
      def view(self, cls=None):
  
          # we need to see if we are subclassing an
          # index type here
          if cls is not None and not hasattr(cls, "_typ"):
              dtype = cls
              if isinstance(cls, str):
                  dtype = pandas_dtype(cls)
  
              if isinstance(dtype, (np.dtype, ExtensionDtype)) and needs_i8_conversion(
                  dtype
              ):
                  if dtype.kind == "m" and dtype != "m8[ns]":
                      # e.g. m8[s]
                      return self._data.view(cls)
  
                  idx_cls = self._dtype_to_subclass(dtype)
                  arr_cls = idx_cls._data_cls
                  arr = arr_cls(self._data.view("i8"), dtype=dtype)
                  return idx_cls._simple_new(arr, name=self.name)
  
              result = self._data.view(cls)
          else:
              result = self._view()
          if isinstance(result, Index):
              result._id = self._id
          return result
  
      def astype(self, dtype, copy=True):
          """
          Create an Index with values cast to dtypes.
  
          The class of a new Index is determined by dtype. When conversion is
          impossible, a TypeError exception is raised.
  
          Parameters
          ----------
          dtype : numpy dtype or pandas type
              Note that any signed integer `dtype` is treated as ``'int64'``,
              and any unsigned integer `dtype` is treated as ``'uint64'``,
              regardless of the size.
          copy : bool, default True
              By default, astype always returns a newly allocated object.
              If copy is set to False and internal requirements on dtype are
              satisfied, the original data is used to create a new Index
              or the original Index is returned.
  
          Returns
          -------
          Index
              Index with values cast to specified dtype.
          """
          if dtype is not None:
              dtype = pandas_dtype(dtype)
  
          if is_dtype_equal(self.dtype, dtype):
              # Ensure that self.astype(self.dtype) is self
              return self.copy() if copy else self
  
          if (
              self.dtype == np.dtype("M8[ns]")
              and isinstance(dtype, np.dtype)
              and dtype.kind == "M"
              and dtype != np.dtype("M8[ns]")
          ):
              # For now DatetimeArray supports this by unwrapping ndarray,
              #  but DatetimeIndex doesn't
              raise TypeError(f"Cannot cast {type(self).__name__} to dtype")
  
          values = self._data
          if isinstance(values, ExtensionArray):
              with rewrite_exception(type(values).__name__, type(self).__name__):
                  new_values = values.astype(dtype, copy=copy)
  
          elif isinstance(dtype, ExtensionDtype):
              cls = dtype.construct_array_type()
              # Note: for RangeIndex and CategoricalDtype self vs self._values
              #  behaves differently here.
              new_values = cls._from_sequence(self, dtype=dtype, copy=copy)
  
          else:
              try:
                  new_values = values.astype(dtype, copy=copy)
              except (TypeError, ValueError) as err:
                  raise TypeError(
                      f"Cannot cast {type(self).__name__} to dtype {dtype}"
                  ) from err
  
          # pass copy=False because any copying will be done in the astype above
          return Index(new_values, name=self.name, dtype=new_values.dtype, copy=False)
  
      _index_shared_docs[
          "take"
      ] = """
          Return a new %(klass)s of the values selected by the indices.
  
          For internal compatibility with numpy arrays.
  
          Parameters
          ----------
          indices : array-like
              Indices to be taken.
          axis : int, optional
              The axis over which to select values, always 0.
          allow_fill : bool, default True
          fill_value : scalar, default None
              If allow_fill=True and fill_value is not None, indices specified by
              -1 are regarded as NA. If Index doesn't hold NA, raise ValueError.
  
          Returns
          -------
          Index
              An index formed of elements at the given indices. Will be the same
              type as self, except for RangeIndex.
  
          See Also
          --------
          numpy.ndarray.take: Return an array formed from the
              elements of a at the given indices.
          """
  
      @Appender(_index_shared_docs["take"] % _index_doc_kwargs)
      def take(
          self, indices, axis: int = 0, allow_fill: bool = True, fill_value=None, **kwargs
      ):
          if kwargs:
              nv.validate_take((), kwargs)
          if is_scalar(indices):
              raise TypeError("Expected indices to be array-like")
          indices = ensure_platform_int(indices)
          allow_fill = self._maybe_disallow_fill(allow_fill, fill_value, indices)
  
          # Note: we discard fill_value and use self._na_value, only relevant
          #  in the case where allow_fill is True and fill_value is not None
          taken = algos.take(
              self._values, indices, allow_fill=allow_fill, fill_value=self._na_value
          )
          # _constructor so RangeIndex->Int64Index
          return self._constructor._simple_new(taken, name=self.name)
  
      @final
      def _maybe_disallow_fill(self, allow_fill: bool, fill_value, indices) -> bool:
          """
          We only use pandas-style take when allow_fill is True _and_
          fill_value is not None.
          """
          if allow_fill and fill_value is not None:
              # only fill if we are passing a non-None fill_value
              if self._can_hold_na:
                  if (indices < -1).any():
                      raise ValueError(
                          "When allow_fill=True and fill_value is not None, "
                          "all indices must be >= -1"
                      )
              else:
                  cls_name = type(self).__name__
                  raise ValueError(
                      f"Unable to fill values because {cls_name} cannot contain NA"
                  )
          else:
              allow_fill = False
          return allow_fill
  
      _index_shared_docs[
          "repeat"
      ] = """
          Repeat elements of a %(klass)s.
  
          Returns a new %(klass)s where each element of the current %(klass)s
          is repeated consecutively a given number of times.
  
          Parameters
          ----------
          repeats : int or array of ints
              The number of repetitions for each element. This should be a
              non-negative integer. Repeating 0 times will return an empty
              %(klass)s.
          axis : None
              Must be ``None``. Has no effect but is accepted for compatibility
              with numpy.
  
          Returns
          -------
          repeated_index : %(klass)s
              Newly created %(klass)s with repeated elements.
  
          See Also
          --------
          Series.repeat : Equivalent function for Series.
          numpy.repeat : Similar method for :class:`numpy.ndarray`.
  
          Examples
          --------
          >>> idx = pd.Index(['a', 'b', 'c'])
          >>> idx
          Index(['a', 'b', 'c'], dtype='object')
          >>> idx.repeat(2)
          Index(['a', 'a', 'b', 'b', 'c', 'c'], dtype='object')
          >>> idx.repeat([1, 2, 3])
          Index(['a', 'b', 'b', 'c', 'c', 'c'], dtype='object')
          """
  
      @Appender(_index_shared_docs["repeat"] % _index_doc_kwargs)
      def repeat(self, repeats, axis=None):
          repeats = ensure_platform_int(repeats)
          nv.validate_repeat((), {"axis": axis})
          res_values = self._values.repeat(repeats)
  
          # _constructor so RangeIndex->Int64Index
          return self._constructor._simple_new(res_values, name=self.name)
  
      # --------------------------------------------------------------------
      # Copying Methods
  
      def copy(
          self: _IndexT,
          name: Hashable | None = None,
          deep: bool = False,
          dtype: Dtype | None = None,
          names: Sequence[Hashable] | None = None,
      ) -> _IndexT:
          """
          Make a copy of this object.
  
          Name and dtype sets those attributes on the new object.
  
          Parameters
          ----------
          name : Label, optional
              Set name for new object.
          deep : bool, default False
          dtype : numpy dtype or pandas type, optional
              Set dtype for new object.
  
              .. deprecated:: 1.2.0
                  use ``astype`` method instead.
          names : list-like, optional
              Kept for compatibility with MultiIndex. Should not be used.
  
              .. deprecated:: 1.4.0
                  use ``name`` instead.
  
          Returns
          -------
          Index
              Index refer to new object which is a copy of this object.
  
          Notes
          -----
          In most cases, there should be no functional difference from using
          ``deep``, but if ``deep`` is passed it will attempt to deepcopy.
          """
          if names is not None:
              warnings.warn(
                  "parameter names is deprecated and will be removed in a future "
                  "version. Use the name parameter instead.",
                  FutureWarning,
                  stacklevel=find_stack_level(),
              )
  
          name = self._validate_names(name=name, names=names, deep=deep)[0]
          if deep:
              new_data = self._data.copy()
              new_index = type(self)._simple_new(new_data, name=name)
          else:
              new_index = self._rename(name=name)
  
          if dtype:
              warnings.warn(
                  "parameter dtype is deprecated and will be removed in a future "
                  "version. Use the astype method instead.",
                  FutureWarning,
                  stacklevel=find_stack_level(),
              )
              new_index = new_index.astype(dtype)
          return new_index
  
      @final
      def __copy__(self: _IndexT, **kwargs) -> _IndexT:
          return self.copy(**kwargs)
  
      @final
      def __deepcopy__(self: _IndexT, memo=None) -> _IndexT:
          """
          Parameters
          ----------
          memo, default None
              Standard signature. Unused
          """
          return self.copy(deep=True)
  
      # --------------------------------------------------------------------
      # Rendering Methods
  
      @final
      def __repr__(self) -> str_t:
          """
          Return a string representation for this object.
          """
          klass_name = type(self).__name__
          data = self._format_data()
          attrs = self._format_attrs()
          space = self._format_space()
          attrs_str = [f"{k}={v}" for k, v in attrs]
          prepr = f",{space}".join(attrs_str)
  
          # no data provided, just attributes
          if data is None:
              data = ""
  
          return f"{klass_name}({data}{prepr})"
  
      def _format_space(self) -> str_t:
  
          # using space here controls if the attributes
          # are line separated or not (the default)
  
          # max_seq_items = get_option('display.max_seq_items')
          # if len(self) > max_seq_items:
          #    space = "\n%s" % (' ' * (len(klass) + 1))
          return " "
  
      @property
      def _formatter_func(self):
          """
          Return the formatter function.
          """
          return default_pprint
  
      def _format_data(self, name=None) -> str_t:
          """
          Return the formatted data as a unicode string.
          """
          # do we want to justify (only do so for non-objects)
          is_justify = True
  
          if self.inferred_type == "string":
              is_justify = False
          elif self.inferred_type == "categorical":
              self = cast("CategoricalIndex", self)
              if is_object_dtype(self.categories):
                  is_justify = False
  
          return format_object_summary(
              self,
              self._formatter_func,
              is_justify=is_justify,
              name=name,
              line_break_each_value=self._is_multi,
          )
  
      def _format_attrs(self) -> list[tuple[str_t, str_t | int | bool | None]]:
          """
          Return a list of tuples of the (attr,formatted_value).
          """
          attrs: list[tuple[str_t, str_t | int | bool | None]] = []
  
          if not self._is_multi:
              attrs.append(("dtype", f"'{self.dtype}'"))
  
          if self.name is not None:
              attrs.append(("name", default_pprint(self.name)))
          elif self._is_multi and any(x is not None for x in self.names):
              attrs.append(("names", default_pprint(self.names)))
  
          max_seq_items = get_option("display.max_seq_items") or len(self)
          if len(self) > max_seq_items:
              attrs.append(("length", len(self)))
          return attrs
  
      @final
      def _mpl_repr(self) -> np.ndarray:
          # how to represent ourselves to matplotlib
          if isinstance(self.dtype, np.dtype) and self.dtype.kind != "M":
              return cast(np.ndarray, self.values)
          return self.astype(object, copy=False)._values
  
      def format(
          self,
          name: bool = False,
          formatter: Callable | None = None,
          na_rep: str_t = "NaN",
      ) -> list[str_t]:
          """
          Render a string representation of the Index.
          """
          header = []
          if name:
              header.append(
                  pprint_thing(self.name, escape_chars=("\t", "\r", "\n"))
                  if self.name is not None
                  else ""
              )
  
          if formatter is not None:
              return header + list(self.map(formatter))
  
          return self._format_with_header(header, na_rep=na_rep)
  
      def _format_with_header(self, header: list[str_t], na_rep: str_t) -> list[str_t]:
          from pandas.io.formats.format import format_array
  
          values = self._values
  
          if is_object_dtype(values.dtype):
              values = cast(np.ndarray, values)
              values = lib.maybe_convert_objects(values, safe=True)
  
              result = [pprint_thing(x, escape_chars=("\t", "\r", "\n")) for x in values]
  
              # could have nans
              mask = isna(values)
              if mask.any():
                  result_arr = np.array(result)
                  result_arr[mask] = na_rep
                  result = result_arr.tolist()
          else:
              result = trim_front(format_array(values, None, justify="left"))
          return header + result
  
      @final
      def to_native_types(self, slicer=None, **kwargs) -> np.ndarray:
          """
          Format specified values of `self` and return them.
  
          .. deprecated:: 1.2.0
  
          Parameters
          ----------
          slicer : int, array-like
              An indexer into `self` that specifies which values
              are used in the formatting process.
          kwargs : dict
              Options for specifying how the values should be formatted.
              These options include the following:
  
              1) na_rep : str
                  The value that serves as a placeholder for NULL values
              2) quoting : bool or None
                  Whether or not there are quoted values in `self`
              3) date_format : str
                  The format used to represent date-like values.
  
          Returns
          -------
          numpy.ndarray
              Formatted values.
          """
          warnings.warn(
              "The 'to_native_types' method is deprecated and will be removed in "
              "a future version. Use 'astype(str)' instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          values = self
          if slicer is not None:
              values = values[slicer]
          return values._format_native_types(**kwargs)
  
      def _format_native_types(self, *, na_rep="", quoting=None, **kwargs):
          """
          Actually format specific types of the index.
          """
          mask = isna(self)
          if not self.is_object() and not quoting:
              values = np.asarray(self).astype(str)
          else:
              values = np.array(self, dtype=object, copy=True)
  
          values[mask] = na_rep
          return values
  
      def _summary(self, name=None) -> str_t:
          """
          Return a summarized representation.
  
          Parameters
          ----------
          name : str
              name to use in the summary representation
  
          Returns
          -------
          String with a summarized representation of the index
          """
          if len(self) > 0:
              head = self[0]
              if hasattr(head, "format") and not isinstance(head, str):
                  head = head.format()
              elif needs_i8_conversion(self.dtype):
                  # e.g. Timedelta, display as values, not quoted
                  head = self._formatter_func(head).replace("'", "")
              tail = self[-1]
              if hasattr(tail, "format") and not isinstance(tail, str):
                  tail = tail.format()
              elif needs_i8_conversion(self.dtype):
                  # e.g. Timedelta, display as values, not quoted
                  tail = self._formatter_func(tail).replace("'", "")
  
              index_summary = f", {head} to {tail}"
          else:
              index_summary = ""
  
          if name is None:
              name = type(self).__name__
          return f"{name}: {len(self)} entries{index_summary}"
  
      # --------------------------------------------------------------------
      # Conversion Methods
  
      def to_flat_index(self):
          """
          Identity method.
  
          This is implemented for compatibility with subclass implementations
          when chaining.
  
          Returns
          -------
          pd.Index
              Caller.
  
          See Also
          --------
          MultiIndex.to_flat_index : Subclass implementation.
          """
          return self
  
      def to_series(self, index=None, name: Hashable = None) -> Series:
          """
          Create a Series with both index and values equal to the index keys.
  
          Useful with map for returning an indexer based on an index.
  
          Parameters
          ----------
          index : Index, optional
              Index of resulting Series. If None, defaults to original index.
          name : str, optional
              Name of resulting Series. If None, defaults to name of original
              index.
  
          Returns
          -------
          Series
              The dtype will be based on the type of the Index values.
  
          See Also
          --------
          Index.to_frame : Convert an Index to a DataFrame.
          Series.to_frame : Convert Series to DataFrame.
  
          Examples
          --------
          >>> idx = pd.Index(['Ant', 'Bear', 'Cow'], name='animal')
  
          By default, the original Index and original name is reused.
  
          >>> idx.to_series()
          animal
          Ant      Ant
          Bear    Bear
          Cow      Cow
          Name: animal, dtype: object
  
          To enforce a new Index, specify new labels to ``index``:
  
          >>> idx.to_series(index=[0, 1, 2])
          0     Ant
          1    Bear
          2     Cow
          Name: animal, dtype: object
  
          To override the name of the resulting column, specify `name`:
  
          >>> idx.to_series(name='zoo')
          animal
          Ant      Ant
          Bear    Bear
          Cow      Cow
          Name: zoo, dtype: object
          """
          from pandas import Series
  
          if index is None:
              index = self._view()
          if name is None:
              name = self.name
  
          return Series(self._values.copy(), index=index, name=name)
  
      def to_frame(
          self, index: bool = True, name: Hashable = lib.no_default
      ) -> DataFrame:
          """
          Create a DataFrame with a column containing the Index.
  
          Parameters
          ----------
          index : bool, default True
              Set the index of the returned DataFrame as the original Index.
  
          name : object, default None
              The passed name should substitute for the index name (if it has
              one).
  
          Returns
          -------
          DataFrame
              DataFrame containing the original Index data.
  
          See Also
          --------
          Index.to_series : Convert an Index to a Series.
          Series.to_frame : Convert Series to DataFrame.
  
          Examples
          --------
          >>> idx = pd.Index(['Ant', 'Bear', 'Cow'], name='animal')
          >>> idx.to_frame()
                 animal
          animal
          Ant       Ant
          Bear     Bear
          Cow       Cow
  
          By default, the original Index is reused. To enforce a new Index:
  
          >>> idx.to_frame(index=False)
              animal
          0   Ant
          1  Bear
          2   Cow
  
          To override the name of the resulting column, specify `name`:
  
          >>> idx.to_frame(index=False, name='zoo')
              zoo
          0   Ant
          1  Bear
          2   Cow
          """
          from pandas import DataFrame
  
          if name is lib.no_default:
              name = self.name or 0
          result = DataFrame({name: self._values.copy()})
  
          if index:
              result.index = self
          return result
  
      # --------------------------------------------------------------------
      # Name-Centric Methods
  
      @property
      def name(self):
          """
          Return Index or MultiIndex name.
          """
          return self._name
  
      @name.setter
      def name(self, value: Hashable):
          if self._no_setting_name:
              # Used in MultiIndex.levels to avoid silently ignoring name updates.
              raise RuntimeError(
                  "Cannot set name on a level of a MultiIndex. Use "
                  "'MultiIndex.set_names' instead."
              )
          maybe_extract_name(value, None, type(self))
          self._name = value
  
      @final
      def _validate_names(
          self, name=None, names=None, deep: bool = False
      ) -> list[Hashable]:
          """
          Handles the quirks of having a singular 'name' parameter for general
          Index and plural 'names' parameter for MultiIndex.
          """
          from copy import deepcopy
  
          if names is not None and name is not None:
              raise TypeError("Can only provide one of `names` and `name`")
          elif names is None and name is None:
              new_names = deepcopy(self.names) if deep else self.names
          elif names is not None:
              if not is_list_like(names):
                  raise TypeError("Must pass list-like as `names`.")
              new_names = names
          elif not is_list_like(name):
              new_names = [name]
          else:
              new_names = name
  
          if len(new_names) != len(self.names):
              raise ValueError(
                  f"Length of new names must be {len(self.names)}, got {len(new_names)}"
              )
  
          # All items in 'new_names' need to be hashable
          validate_all_hashable(*new_names, error_name=f"{type(self).__name__}.name")
  
          return new_names
  
      def _get_names(self) -> FrozenList:
          return FrozenList((self.name,))
  
      def _set_names(self, values, *, level=None) -> None:
          """
          Set new names on index. Each name has to be a hashable type.
  
          Parameters
          ----------
          values : str or sequence
              name(s) to set
          level : int, level name, or sequence of int/level names (default None)
              If the index is a MultiIndex (hierarchical), level(s) to set (None
              for all levels).  Otherwise level must be None
  
          Raises
          ------
          TypeError if each name is not hashable.
          """
          if not is_list_like(values):
              raise ValueError("Names must be a list-like")
          if len(values) != 1:
              raise ValueError(f"Length of new names must be 1, got {len(values)}")
  
          # GH 20527
          # All items in 'name' need to be hashable:
          validate_all_hashable(*values, error_name=f"{type(self).__name__}.name")
  
          self._name = values[0]
  
      names = property(fset=_set_names, fget=_get_names)
  
      @deprecate_nonkeyword_arguments(version=None, allowed_args=["self", "names"])
      def set_names(self, names, level=None, inplace: bool = False):
          """
          Set Index or MultiIndex name.
  
          Able to set new names partially and by level.
  
          Parameters
          ----------
  
          names : label or list of label or dict-like for MultiIndex
              Name(s) to set.
  
              .. versionchanged:: 1.3.0
  
          level : int, label or list of int or label, optional
              If the index is a MultiIndex and names is not dict-like, level(s) to set
              (None for all levels). Otherwise level must be None.
  
              .. versionchanged:: 1.3.0
  
          inplace : bool, default False
              Modifies the object directly, instead of creating a new Index or
              MultiIndex.
  
          Returns
          -------
          Index or None
              The same type as the caller or None if ``inplace=True``.
  
          See Also
          --------
          Index.rename : Able to set new names without level.
  
          Examples
          --------
          >>> idx = pd.Index([1, 2, 3, 4])
          >>> idx
          Int64Index([1, 2, 3, 4], dtype='int64')
          >>> idx.set_names('quarter')
          Int64Index([1, 2, 3, 4], dtype='int64', name='quarter')
  
          >>> idx = pd.MultiIndex.from_product([['python', 'cobra'],
          ...                                   [2018, 2019]])
          >>> idx
          MultiIndex([('python', 2018),
                      ('python', 2019),
                      ( 'cobra', 2018),
                      ( 'cobra', 2019)],
                     )
          >>> idx.set_names(['kind', 'year'], inplace=True)
          >>> idx
          MultiIndex([('python', 2018),
                      ('python', 2019),
                      ( 'cobra', 2018),
                      ( 'cobra', 2019)],
                     names=['kind', 'year'])
          >>> idx.set_names('species', level=0)
          MultiIndex([('python', 2018),
                      ('python', 2019),
                      ( 'cobra', 2018),
                      ( 'cobra', 2019)],
                     names=['species', 'year'])
  
          When renaming levels with a dict, levels can not be passed.
  
          >>> idx.set_names({'kind': 'snake'})
          MultiIndex([('python', 2018),
                      ('python', 2019),
                      ( 'cobra', 2018),
                      ( 'cobra', 2019)],
                     names=['snake', 'year'])
          """
          if level is not None and not isinstance(self, ABCMultiIndex):
              raise ValueError("Level must be None for non-MultiIndex")
  
          elif level is not None and not is_list_like(level) and is_list_like(names):
              raise TypeError("Names must be a string when a single level is provided.")
  
          elif not is_list_like(names) and level is None and self.nlevels > 1:
              raise TypeError("Must pass list-like as `names`.")
  
          elif is_dict_like(names) and not isinstance(self, ABCMultiIndex):
              raise TypeError("Can only pass dict-like as `names` for MultiIndex.")
  
          elif is_dict_like(names) and level is not None:
              raise TypeError("Can not pass level for dictlike `names`.")
  
          if isinstance(self, ABCMultiIndex) and is_dict_like(names) and level is None:
              # Transform dict to list of new names and corresponding levels
              level, names_adjusted = [], []
              for i, name in enumerate(self.names):
                  if name in names.keys():
                      level.append(i)
                      names_adjusted.append(names[name])
              names = names_adjusted
  
          if not is_list_like(names):
              names = [names]
          if level is not None and not is_list_like(level):
              level = [level]
  
          if inplace:
              idx = self
          else:
              idx = self._view()
  
          idx._set_names(names, level=level)
          if not inplace:
              return idx
  
      def rename(self, name, inplace=False):
          """
          Alter Index or MultiIndex name.
  
          Able to set new names without level. Defaults to returning new index.
          Length of names must match number of levels in MultiIndex.
  
          Parameters
          ----------
          name : label or list of labels
              Name(s) to set.
          inplace : bool, default False
              Modifies the object directly, instead of creating a new Index or
              MultiIndex.
  
          Returns
          -------
          Index or None
              The same type as the caller or None if ``inplace=True``.
  
          See Also
          --------
          Index.set_names : Able to set new names partially and by level.
  
          Examples
          --------
          >>> idx = pd.Index(['A', 'C', 'A', 'B'], name='score')
          >>> idx.rename('grade')
          Index(['A', 'C', 'A', 'B'], dtype='object', name='grade')
  
          >>> idx = pd.MultiIndex.from_product([['python', 'cobra'],
          ...                                   [2018, 2019]],
          ...                                   names=['kind', 'year'])
          >>> idx
          MultiIndex([('python', 2018),
                      ('python', 2019),
                      ( 'cobra', 2018),
                      ( 'cobra', 2019)],
                     names=['kind', 'year'])
          >>> idx.rename(['species', 'year'])
          MultiIndex([('python', 2018),
                      ('python', 2019),
                      ( 'cobra', 2018),
                      ( 'cobra', 2019)],
                     names=['species', 'year'])
          >>> idx.rename('species')
          Traceback (most recent call last):
          TypeError: Must pass list-like as `names`.
          """
          return self.set_names([name], inplace=inplace)
  
      # --------------------------------------------------------------------
      # Level-Centric Methods
  
      @property
      def nlevels(self) -> int:
          """
          Number of levels.
          """
          return 1
  
      def _sort_levels_monotonic(self: _IndexT) -> _IndexT:
          """
          Compat with MultiIndex.
          """
          return self
  
      @final
      def _validate_index_level(self, level) -> None:
          """
          Validate index level.
  
          For single-level Index getting level number is a no-op, but some
          verification must be done like in MultiIndex.
  
          """
          if isinstance(level, int):
              if level < 0 and level != -1:
                  raise IndexError(
                      "Too many levels: Index has only 1 level, "
                      f"{level} is not a valid level number"
                  )
              elif level > 0:
                  raise IndexError(
                      f"Too many levels: Index has only 1 level, not {level + 1}"
                  )
          elif level != self.name:
              raise KeyError(
                  f"Requested level ({level}) does not match index name ({self.name})"
              )
  
      def _get_level_number(self, level) -> int:
          self._validate_index_level(level)
          return 0
  
      def sortlevel(self, level=None, ascending=True, sort_remaining=None):
          """
          For internal compatibility with the Index API.
  
          Sort the Index. This is for compat with MultiIndex
  
          Parameters
          ----------
          ascending : bool, default True
              False to sort in descending order
  
          level, sort_remaining are compat parameters
  
          Returns
          -------
          Index
          """
          if not isinstance(ascending, (list, bool)):
              raise TypeError(
                  "ascending must be a single bool value or"
                  "a list of bool values of length 1"
              )
  
          if isinstance(ascending, list):
              if len(ascending) != 1:
                  raise TypeError("ascending must be a list of bool values of length 1")
              ascending = ascending[0]
  
          if not isinstance(ascending, bool):
              raise TypeError("ascending must be a bool value")
  
          return self.sort_values(return_indexer=True, ascending=ascending)
  
      def _get_level_values(self, level) -> Index:
          """
          Return an Index of values for requested level.
  
          This is primarily useful to get an individual level of values from a
          MultiIndex, but is provided on Index as well for compatibility.
  
          Parameters
          ----------
          level : int or str
              It is either the integer position or the name of the level.
  
          Returns
          -------
          Index
              Calling object, as there is only one level in the Index.
  
          See Also
          --------
          MultiIndex.get_level_values : Get values for a level of a MultiIndex.
  
          Notes
          -----
          For Index, level should be 0, since there are no multiple levels.
  
          Examples
          --------
          >>> idx = pd.Index(list('abc'))
          >>> idx
          Index(['a', 'b', 'c'], dtype='object')
  
          Get level values by supplying `level` as integer:
  
          >>> idx.get_level_values(0)
          Index(['a', 'b', 'c'], dtype='object')
          """
          self._validate_index_level(level)
          return self
  
      get_level_values = _get_level_values
  
      @final
      def droplevel(self, level=0):
          """
          Return index with requested level(s) removed.
  
          If resulting index has only 1 level left, the result will be
          of Index type, not MultiIndex.
  
          Parameters
          ----------
          level : int, str, or list-like, default 0
              If a string is given, must be the name of a level
              If list-like, elements must be names or indexes of levels.
  
          Returns
          -------
          Index or MultiIndex
  
          Examples
          --------
          >>> mi = pd.MultiIndex.from_arrays(
          ... [[1, 2], [3, 4], [5, 6]], names=['x', 'y', 'z'])
          >>> mi
          MultiIndex([(1, 3, 5),
                      (2, 4, 6)],
                     names=['x', 'y', 'z'])
  
          >>> mi.droplevel()
          MultiIndex([(3, 5),
                      (4, 6)],
                     names=['y', 'z'])
  
          >>> mi.droplevel(2)
          MultiIndex([(1, 3),
                      (2, 4)],
                     names=['x', 'y'])
  
          >>> mi.droplevel('z')
          MultiIndex([(1, 3),
                      (2, 4)],
                     names=['x', 'y'])
  
          >>> mi.droplevel(['x', 'y'])
          Int64Index([5, 6], dtype='int64', name='z')
          """
          if not isinstance(level, (tuple, list)):
              level = [level]
  
          levnums = sorted(self._get_level_number(lev) for lev in level)[::-1]
  
          return self._drop_level_numbers(levnums)
  
      @final
      def _drop_level_numbers(self, levnums: list[int]):
          """
          Drop MultiIndex levels by level _number_, not name.
          """
  
          if not levnums and not isinstance(self, ABCMultiIndex):
              return self
          if len(levnums) >= self.nlevels:
              raise ValueError(
                  f"Cannot remove {len(levnums)} levels from an index with "
                  f"{self.nlevels} levels: at least one level must be left."
              )
          # The two checks above guarantee that here self is a MultiIndex
          self = cast("MultiIndex", self)
  
          new_levels = list(self.levels)
          new_codes = list(self.codes)
          new_names = list(self.names)
  
          for i in levnums:
              new_levels.pop(i)
              new_codes.pop(i)
              new_names.pop(i)
  
          if len(new_levels) == 1:
              lev = new_levels[0]
  
              if len(lev) == 0:
                  # If lev is empty, lev.take will fail GH#42055
                  res_values = algos.take(lev._values, new_codes[0], allow_fill=True)
                  result = type(lev)._simple_new(res_values, name=new_names[0])
              else:
                  # set nan if needed
                  mask = new_codes[0] == -1
                  result = new_levels[0].take(new_codes[0])
                  if mask.any():
                      result = result.putmask(mask, np.nan)
  
                  result._name = new_names[0]
  
              return result
          else:
              from pandas.core.indexes.multi import MultiIndex
  
              return MultiIndex(
                  levels=new_levels,
                  codes=new_codes,
                  names=new_names,
                  verify_integrity=False,
              )
  
      def _get_grouper_for_level(self, mapper, *, level=None):
          """
          Get index grouper corresponding to an index level
  
          Parameters
          ----------
          mapper: Group mapping function or None
              Function mapping index values to groups
          level : int or None
              Index level, positional
  
          Returns
          -------
          grouper : Index
              Index of values to group on.
          labels : ndarray of int or None
              Array of locations in level_index.
          uniques : Index or None
              Index of unique values for level.
          """
          assert level is None or level == 0
          if mapper is None:
              grouper = self
          else:
              grouper = self.map(mapper)
  
          return grouper, None, None
  
      # --------------------------------------------------------------------
      # Introspection Methods
  
      @final
      @property
      def is_monotonic(self) -> bool:
          """
          Alias for is_monotonic_increasing.
          """
          return self.is_monotonic_increasing
  
      @property
      def is_monotonic_increasing(self) -> bool:
          """
          Return if the index is monotonic increasing (only equal or
          increasing) values.
  
          Examples
          --------
          >>> Index([1, 2, 3]).is_monotonic_increasing
          True
          >>> Index([1, 2, 2]).is_monotonic_increasing
          True
          >>> Index([1, 3, 2]).is_monotonic_increasing
          False
          """
          return self._engine.is_monotonic_increasing
  
      @property
      def is_monotonic_decreasing(self) -> bool:
          """
          Return if the index is monotonic decreasing (only equal or
          decreasing) values.
  
          Examples
          --------
          >>> Index([3, 2, 1]).is_monotonic_decreasing
          True
          >>> Index([3, 2, 2]).is_monotonic_decreasing
          True
          >>> Index([3, 1, 2]).is_monotonic_decreasing
          False
          """
          return self._engine.is_monotonic_decreasing
  
      @final
      @property
      def _is_strictly_monotonic_increasing(self) -> bool:
          """
          Return if the index is strictly monotonic increasing
          (only increasing) values.
  
          Examples
          --------
          >>> Index([1, 2, 3])._is_strictly_monotonic_increasing
          True
          >>> Index([1, 2, 2])._is_strictly_monotonic_increasing
          False
          >>> Index([1, 3, 2])._is_strictly_monotonic_increasing
          False
          """
          return self.is_unique and self.is_monotonic_increasing
  
      @final
      @property
      def _is_strictly_monotonic_decreasing(self) -> bool:
          """
          Return if the index is strictly monotonic decreasing
          (only decreasing) values.
  
          Examples
          --------
          >>> Index([3, 2, 1])._is_strictly_monotonic_decreasing
          True
          >>> Index([3, 2, 2])._is_strictly_monotonic_decreasing
          False
          >>> Index([3, 1, 2])._is_strictly_monotonic_decreasing
          False
          """
          return self.is_unique and self.is_monotonic_decreasing
  
      @cache_readonly
      def is_unique(self) -> bool:
          """
          Return if the index has unique values.
          """
          return self._engine.is_unique
  
      @final
      @property
      def has_duplicates(self) -> bool:
          """
          Check if the Index has duplicate values.
  
          Returns
          -------
          bool
              Whether or not the Index has duplicate values.
  
          Examples
          --------
          >>> idx = pd.Index([1, 5, 7, 7])
          >>> idx.has_duplicates
          True
  
          >>> idx = pd.Index([1, 5, 7])
          >>> idx.has_duplicates
          False
  
          >>> idx = pd.Index(["Watermelon", "Orange", "Apple",
          ...                 "Watermelon"]).astype("category")
          >>> idx.has_duplicates
          True
  
          >>> idx = pd.Index(["Orange", "Apple",
          ...                 "Watermelon"]).astype("category")
          >>> idx.has_duplicates
          False
          """
          return not self.is_unique
  
      @final
      def is_boolean(self) -> bool:
          """
          Check if the Index only consists of booleans.
  
          Returns
          -------
          bool
              Whether or not the Index only consists of booleans.
  
          See Also
          --------
          is_integer : Check if the Index only consists of integers.
          is_floating : Check if the Index is a floating type.
          is_numeric : Check if the Index only consists of numeric data.
          is_object : Check if the Index is of the object dtype.
          is_categorical : Check if the Index holds categorical data.
          is_interval : Check if the Index holds Interval objects.
          is_mixed : Check if the Index holds data with mixed data types.
  
          Examples
          --------
          >>> idx = pd.Index([True, False, True])
          >>> idx.is_boolean()
          True
  
          >>> idx = pd.Index(["True", "False", "True"])
          >>> idx.is_boolean()
          False
  
          >>> idx = pd.Index([True, False, "True"])
          >>> idx.is_boolean()
          False
          """
          return self.inferred_type in ["boolean"]
  
      @final
      def is_integer(self) -> bool:
          """
          Check if the Index only consists of integers.
  
          Returns
          -------
          bool
              Whether or not the Index only consists of integers.
  
          See Also
          --------
          is_boolean : Check if the Index only consists of booleans.
          is_floating : Check if the Index is a floating type.
          is_numeric : Check if the Index only consists of numeric data.
          is_object : Check if the Index is of the object dtype.
          is_categorical : Check if the Index holds categorical data.
          is_interval : Check if the Index holds Interval objects.
          is_mixed : Check if the Index holds data with mixed data types.
  
          Examples
          --------
          >>> idx = pd.Index([1, 2, 3, 4])
          >>> idx.is_integer()
          True
  
          >>> idx = pd.Index([1.0, 2.0, 3.0, 4.0])
          >>> idx.is_integer()
          False
  
          >>> idx = pd.Index(["Apple", "Mango", "Watermelon"])
          >>> idx.is_integer()
          False
          """
          return self.inferred_type in ["integer"]
  
      @final
      def is_floating(self) -> bool:
          """
          Check if the Index is a floating type.
  
          The Index may consist of only floats, NaNs, or a mix of floats,
          integers, or NaNs.
  
          Returns
          -------
          bool
              Whether or not the Index only consists of only consists of floats, NaNs, or
              a mix of floats, integers, or NaNs.
  
          See Also
          --------
          is_boolean : Check if the Index only consists of booleans.
          is_integer : Check if the Index only consists of integers.
          is_numeric : Check if the Index only consists of numeric data.
          is_object : Check if the Index is of the object dtype.
          is_categorical : Check if the Index holds categorical data.
          is_interval : Check if the Index holds Interval objects.
          is_mixed : Check if the Index holds data with mixed data types.
  
          Examples
          --------
          >>> idx = pd.Index([1.0, 2.0, 3.0, 4.0])
          >>> idx.is_floating()
          True
  
          >>> idx = pd.Index([1.0, 2.0, np.nan, 4.0])
          >>> idx.is_floating()
          True
  
          >>> idx = pd.Index([1, 2, 3, 4, np.nan])
          >>> idx.is_floating()
          True
  
          >>> idx = pd.Index([1, 2, 3, 4])
          >>> idx.is_floating()
          False
          """
          return self.inferred_type in ["floating", "mixed-integer-float", "integer-na"]
  
      @final
      def is_numeric(self) -> bool:
          """
          Check if the Index only consists of numeric data.
  
          Returns
          -------
          bool
              Whether or not the Index only consists of numeric data.
  
          See Also
          --------
          is_boolean : Check if the Index only consists of booleans.
          is_integer : Check if the Index only consists of integers.
          is_floating : Check if the Index is a floating type.
          is_object : Check if the Index is of the object dtype.
          is_categorical : Check if the Index holds categorical data.
          is_interval : Check if the Index holds Interval objects.
          is_mixed : Check if the Index holds data with mixed data types.
  
          Examples
          --------
          >>> idx = pd.Index([1.0, 2.0, 3.0, 4.0])
          >>> idx.is_numeric()
          True
  
          >>> idx = pd.Index([1, 2, 3, 4.0])
          >>> idx.is_numeric()
          True
  
          >>> idx = pd.Index([1, 2, 3, 4])
          >>> idx.is_numeric()
          True
  
          >>> idx = pd.Index([1, 2, 3, 4.0, np.nan])
          >>> idx.is_numeric()
          True
  
          >>> idx = pd.Index([1, 2, 3, 4.0, np.nan, "Apple"])
          >>> idx.is_numeric()
          False
          """
          return self.inferred_type in ["integer", "floating"]
  
      @final
      def is_object(self) -> bool:
          """
          Check if the Index is of the object dtype.
  
          Returns
          -------
          bool
              Whether or not the Index is of the object dtype.
  
          See Also
          --------
          is_boolean : Check if the Index only consists of booleans.
          is_integer : Check if the Index only consists of integers.
          is_floating : Check if the Index is a floating type.
          is_numeric : Check if the Index only consists of numeric data.
          is_categorical : Check if the Index holds categorical data.
          is_interval : Check if the Index holds Interval objects.
          is_mixed : Check if the Index holds data with mixed data types.
  
          Examples
          --------
          >>> idx = pd.Index(["Apple", "Mango", "Watermelon"])
          >>> idx.is_object()
          True
  
          >>> idx = pd.Index(["Apple", "Mango", 2.0])
          >>> idx.is_object()
          True
  
          >>> idx = pd.Index(["Watermelon", "Orange", "Apple",
          ...                 "Watermelon"]).astype("category")
          >>> idx.is_object()
          False
  
          >>> idx = pd.Index([1.0, 2.0, 3.0, 4.0])
          >>> idx.is_object()
          False
          """
          return is_object_dtype(self.dtype)
  
      @final
      def is_categorical(self) -> bool:
          """
          Check if the Index holds categorical data.
  
          Returns
          -------
          bool
              True if the Index is categorical.
  
          See Also
          --------
          CategoricalIndex : Index for categorical data.
          is_boolean : Check if the Index only consists of booleans.
          is_integer : Check if the Index only consists of integers.
          is_floating : Check if the Index is a floating type.
          is_numeric : Check if the Index only consists of numeric data.
          is_object : Check if the Index is of the object dtype.
          is_interval : Check if the Index holds Interval objects.
          is_mixed : Check if the Index holds data with mixed data types.
  
          Examples
          --------
          >>> idx = pd.Index(["Watermelon", "Orange", "Apple",
          ...                 "Watermelon"]).astype("category")
          >>> idx.is_categorical()
          True
  
          >>> idx = pd.Index([1, 3, 5, 7])
          >>> idx.is_categorical()
          False
  
          >>> s = pd.Series(["Peter", "Victor", "Elisabeth", "Mar"])
          >>> s
          0        Peter
          1       Victor
          2    Elisabeth
          3          Mar
          dtype: object
          >>> s.index.is_categorical()
          False
          """
          return self.inferred_type in ["categorical"]
  
      @final
      def is_interval(self) -> bool:
          """
          Check if the Index holds Interval objects.
  
          Returns
          -------
          bool
              Whether or not the Index holds Interval objects.
  
          See Also
          --------
          IntervalIndex : Index for Interval objects.
          is_boolean : Check if the Index only consists of booleans.
          is_integer : Check if the Index only consists of integers.
          is_floating : Check if the Index is a floating type.
          is_numeric : Check if the Index only consists of numeric data.
          is_object : Check if the Index is of the object dtype.
          is_categorical : Check if the Index holds categorical data.
          is_mixed : Check if the Index holds data with mixed data types.
  
          Examples
          --------
          >>> idx = pd.Index([pd.Interval(left=0, right=5),
          ...                 pd.Interval(left=5, right=10)])
          >>> idx.is_interval()
          True
  
          >>> idx = pd.Index([1, 3, 5, 7])
          >>> idx.is_interval()
          False
          """
          return self.inferred_type in ["interval"]
  
      @final
      def is_mixed(self) -> bool:
          """
          Check if the Index holds data with mixed data types.
  
          Returns
          -------
          bool
              Whether or not the Index holds data with mixed data types.
  
          See Also
          --------
          is_boolean : Check if the Index only consists of booleans.
          is_integer : Check if the Index only consists of integers.
          is_floating : Check if the Index is a floating type.
          is_numeric : Check if the Index only consists of numeric data.
          is_object : Check if the Index is of the object dtype.
          is_categorical : Check if the Index holds categorical data.
          is_interval : Check if the Index holds Interval objects.
  
          Examples
          --------
          >>> idx = pd.Index(['a', np.nan, 'b'])
          >>> idx.is_mixed()
          True
  
          >>> idx = pd.Index([1.0, 2.0, 3.0, 5.0])
          >>> idx.is_mixed()
          False
          """
          warnings.warn(
              "Index.is_mixed is deprecated and will be removed in a future version. "
              "Check index.inferred_type directly instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          return self.inferred_type in ["mixed"]
  
      @final
      def holds_integer(self) -> bool:
          """
          Whether the type is an integer type.
          """
          return self.inferred_type in ["integer", "mixed-integer"]
  
      @cache_readonly
      def inferred_type(self) -> str_t:
          """
          Return a string of the type inferred from the values.
          """
          return lib.infer_dtype(self._values, skipna=False)
  
      @cache_readonly
      def _is_all_dates(self) -> bool:
          """
          Whether or not the index values only consist of dates.
          """
          return is_datetime_array(ensure_object(self._values))
  
      @cache_readonly
      @final
      def is_all_dates(self) -> bool:
          """
          Whether or not the index values only consist of dates.
          """
          warnings.warn(
              "Index.is_all_dates is deprecated, will be removed in a future version. "
              "check index.inferred_type instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          return self._is_all_dates
  
      @final
      @cache_readonly
      def _is_multi(self) -> bool:
          """
          Cached check equivalent to isinstance(self, MultiIndex)
          """
          return isinstance(self, ABCMultiIndex)
  
      # --------------------------------------------------------------------
      # Pickle Methods
  
      def __reduce__(self):
          d = {"data": self._data, "name": self.name}
          return _new_Index, (type(self), d), None
  
      # --------------------------------------------------------------------
      # Null Handling Methods
  
      _na_value: float | NaTType = np.nan
      """The expected NA value to use with this index."""
  
      @cache_readonly
      def _isnan(self) -> npt.NDArray[np.bool_]:
          """
          Return if each value is NaN.
          """
          if self._can_hold_na:
              return isna(self)
          else:
              # shouldn't reach to this condition by checking hasnans beforehand
              values = np.empty(len(self), dtype=np.bool_)
              values.fill(False)
              return values
  
      @cache_readonly
      def hasnans(self) -> bool:
          """
          Return if I have any nans; enables various perf speedups.
          """
          if self._can_hold_na:
              return bool(self._isnan.any())
          else:
              return False
  
      @final
      def isna(self) -> npt.NDArray[np.bool_]:
          """
          Detect missing values.
  
          Return a boolean same-sized object indicating if the values are NA.
          NA values, such as ``None``, :attr:`numpy.NaN` or :attr:`pd.NaT`, get
          mapped to ``True`` values.
          Everything else get mapped to ``False`` values. Characters such as
          empty strings `''` or :attr:`numpy.inf` are not considered NA values
          (unless you set ``pandas.options.mode.use_inf_as_na = True``).
  
          Returns
          -------
          numpy.ndarray[bool]
              A boolean array of whether my values are NA.
  
          See Also
          --------
          Index.notna : Boolean inverse of isna.
          Index.dropna : Omit entries with missing values.
          isna : Top-level isna.
          Series.isna : Detect missing values in Series object.
  
          Examples
          --------
          Show which entries in a pandas.Index are NA. The result is an
          array.
  
          >>> idx = pd.Index([5.2, 6.0, np.NaN])
          >>> idx
          Float64Index([5.2, 6.0, nan], dtype='float64')
          >>> idx.isna()
          array([False, False,  True])
  
          Empty strings are not considered NA values. None is considered an NA
          value.
  
          >>> idx = pd.Index(['black', '', 'red', None])
          >>> idx
          Index(['black', '', 'red', None], dtype='object')
          >>> idx.isna()
          array([False, False, False,  True])
  
          For datetimes, `NaT` (Not a Time) is considered as an NA value.
  
          >>> idx = pd.DatetimeIndex([pd.Timestamp('1940-04-25'),
          ...                         pd.Timestamp(''), None, pd.NaT])
          >>> idx
          DatetimeIndex(['1940-04-25', 'NaT', 'NaT', 'NaT'],
                        dtype='datetime64[ns]', freq=None)
          >>> idx.isna()
          array([False,  True,  True,  True])
          """
          return self._isnan
  
      isnull = isna
  
      @final
      def notna(self) -> npt.NDArray[np.bool_]:
          """
          Detect existing (non-missing) values.
  
          Return a boolean same-sized object indicating if the values are not NA.
          Non-missing values get mapped to ``True``. Characters such as empty
          strings ``''`` or :attr:`numpy.inf` are not considered NA values
          (unless you set ``pandas.options.mode.use_inf_as_na = True``).
          NA values, such as None or :attr:`numpy.NaN`, get mapped to ``False``
          values.
  
          Returns
          -------
          numpy.ndarray[bool]
              Boolean array to indicate which entries are not NA.
  
          See Also
          --------
          Index.notnull : Alias of notna.
          Index.isna: Inverse of notna.
          notna : Top-level notna.
  
          Examples
          --------
          Show which entries in an Index are not NA. The result is an
          array.
  
          >>> idx = pd.Index([5.2, 6.0, np.NaN])
          >>> idx
          Float64Index([5.2, 6.0, nan], dtype='float64')
          >>> idx.notna()
          array([ True,  True, False])
  
          Empty strings are not considered NA values. None is considered a NA
          value.
  
          >>> idx = pd.Index(['black', '', 'red', None])
          >>> idx
          Index(['black', '', 'red', None], dtype='object')
          >>> idx.notna()
          array([ True,  True,  True, False])
          """
          return ~self.isna()
  
      notnull = notna
  
      def fillna(self, value=None, downcast=None):
          """
          Fill NA/NaN values with the specified value.
  
          Parameters
          ----------
          value : scalar
              Scalar value to use to fill holes (e.g. 0).
              This value cannot be a list-likes.
          downcast : dict, default is None
              A dict of item->dtype of what to downcast if possible,
              or the string 'infer' which will try to downcast to an appropriate
              equal type (e.g. float64 to int64 if possible).
  
          Returns
          -------
          Index
  
          See Also
          --------
          DataFrame.fillna : Fill NaN values of a DataFrame.
          Series.fillna : Fill NaN Values of a Series.
          """
  
          value = self._require_scalar(value)
          if self.hasnans:
              result = self.putmask(self._isnan, value)
              if downcast is None:
                  # no need to care metadata other than name
                  # because it can't have freq if it has NaTs
                  return Index._with_infer(result, name=self.name)
              raise NotImplementedError(
                  f"{type(self).__name__}.fillna does not support 'downcast' "
                  "argument values other than 'None'."
              )
          return self._view()
  
      def dropna(self: _IndexT, how: str_t = "any") -> _IndexT:
          """
          Return Index without NA/NaN values.
  
          Parameters
          ----------
          how : {'any', 'all'}, default 'any'
              If the Index is a MultiIndex, drop the value when any or all levels
              are NaN.
  
          Returns
          -------
          Index
          """
          if how not in ("any", "all"):
              raise ValueError(f"invalid how option: {how}")
  
          if self.hasnans:
              res_values = self._values[~self._isnan]
              return type(self)._simple_new(res_values, name=self.name)
          return self._view()
  
      # --------------------------------------------------------------------
      # Uniqueness Methods
  
      def unique(self: _IndexT, level: Hashable | None = None) -> _IndexT:
          """
          Return unique values in the index.
  
          Unique values are returned in order of appearance, this does NOT sort.
  
          Parameters
          ----------
          level : int or hashable, optional
              Only return values from specified level (for MultiIndex).
              If int, gets the level by integer position, else by level name.
  
          Returns
          -------
          Index
  
          See Also
          --------
          unique : Numpy array of unique values in that column.
          Series.unique : Return unique values of Series object.
          """
          if level is not None:
              self._validate_index_level(level)
  
          if self.is_unique:
              return self._view()
  
          result = super().unique()
          return self._shallow_copy(result)
  
      @deprecate_nonkeyword_arguments(version=None, allowed_args=["self"])
      def drop_duplicates(self: _IndexT, keep: str_t | bool = "first") -> _IndexT:
          """
          Return Index with duplicate values removed.
  
          Parameters
          ----------
          keep : {'first', 'last', ``False``}, default 'first'
              - 'first' : Drop duplicates except for the first occurrence.
              - 'last' : Drop duplicates except for the last occurrence.
              - ``False`` : Drop all duplicates.
  
          Returns
          -------
          deduplicated : Index
  
          See Also
          --------
          Series.drop_duplicates : Equivalent method on Series.
          DataFrame.drop_duplicates : Equivalent method on DataFrame.
          Index.duplicated : Related method on Index, indicating duplicate
              Index values.
  
          Examples
          --------
          Generate an pandas.Index with duplicate values.
  
          >>> idx = pd.Index(['lama', 'cow', 'lama', 'beetle', 'lama', 'hippo'])
  
          The `keep` parameter controls  which duplicate values are removed.
          The value 'first' keeps the first occurrence for each
          set of duplicated entries. The default value of keep is 'first'.
  
          >>> idx.drop_duplicates(keep='first')
          Index(['lama', 'cow', 'beetle', 'hippo'], dtype='object')
  
          The value 'last' keeps the last occurrence for each set of duplicated
          entries.
  
          >>> idx.drop_duplicates(keep='last')
          Index(['cow', 'beetle', 'lama', 'hippo'], dtype='object')
  
          The value ``False`` discards all sets of duplicated entries.
  
          >>> idx.drop_duplicates(keep=False)
          Index(['cow', 'beetle', 'hippo'], dtype='object')
          """
          if self.is_unique:
              return self._view()
  
          return super().drop_duplicates(keep=keep)
  
      def duplicated(
          self, keep: Literal["first", "last", False] = "first"
      ) -> npt.NDArray[np.bool_]:
          """
          Indicate duplicate index values.
  
          Duplicated values are indicated as ``True`` values in the resulting
          array. Either all duplicates, all except the first, or all except the
          last occurrence of duplicates can be indicated.
  
          Parameters
          ----------
          keep : {'first', 'last', False}, default 'first'
              The value or values in a set of duplicates to mark as missing.
  
              - 'first' : Mark duplicates as ``True`` except for the first
                occurrence.
              - 'last' : Mark duplicates as ``True`` except for the last
                occurrence.
              - ``False`` : Mark all duplicates as ``True``.
  
          Returns
          -------
          np.ndarray[bool]
  
          See Also
          --------
          Series.duplicated : Equivalent method on pandas.Series.
          DataFrame.duplicated : Equivalent method on pandas.DataFrame.
          Index.drop_duplicates : Remove duplicate values from Index.
  
          Examples
          --------
          By default, for each set of duplicated values, the first occurrence is
          set to False and all others to True:
  
          >>> idx = pd.Index(['lama', 'cow', 'lama', 'beetle', 'lama'])
          >>> idx.duplicated()
          array([False, False,  True, False,  True])
  
          which is equivalent to
  
          >>> idx.duplicated(keep='first')
          array([False, False,  True, False,  True])
  
          By using 'last', the last occurrence of each set of duplicated values
          is set on False and all others on True:
  
          >>> idx.duplicated(keep='last')
          array([ True, False,  True, False, False])
  
          By setting keep on ``False``, all duplicates are True:
  
          >>> idx.duplicated(keep=False)
          array([ True, False,  True, False,  True])
          """
          if self.is_unique:
              # fastpath available bc we are immutable
              return np.zeros(len(self), dtype=bool)
          return self._duplicated(keep=keep)
  
      # --------------------------------------------------------------------
      # Arithmetic & Logical Methods
  
      def __iadd__(self, other):
          # alias for __add__
          return self + other
  
      @final
      def __and__(self, other):
          warnings.warn(
              "Index.__and__ operating as a set operation is deprecated, "
              "in the future this will be a logical operation matching "
              "Series.__and__.  Use index.intersection(other) instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          return self.intersection(other)
  
      @final
      def __or__(self, other):
          warnings.warn(
              "Index.__or__ operating as a set operation is deprecated, "
              "in the future this will be a logical operation matching "
              "Series.__or__.  Use index.union(other) instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          return self.union(other)
  
      @final
      def __xor__(self, other):
          warnings.warn(
              "Index.__xor__ operating as a set operation is deprecated, "
              "in the future this will be a logical operation matching "
              "Series.__xor__.  Use index.symmetric_difference(other) instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          return self.symmetric_difference(other)
  
      @final
      def __nonzero__(self):
          raise ValueError(
              f"The truth value of a {type(self).__name__} is ambiguous. "
              "Use a.empty, a.bool(), a.item(), a.any() or a.all()."
          )
  
      __bool__ = __nonzero__
  
      # --------------------------------------------------------------------
      # Set Operation Methods
  
      def _get_reconciled_name_object(self, other):
          """
          If the result of a set operation will be self,
          return self, unless the name changes, in which
          case make a shallow copy of self.
          """
          name = get_op_result_name(self, other)
          if self.name is not name:
              return self.rename(name)
          return self
  
      @final
      def _validate_sort_keyword(self, sort):
          if sort not in [None, False]:
              raise ValueError(
                  "The 'sort' keyword only takes the values of "
                  f"None or False; {sort} was passed."
              )
  
      @final
      def union(self, other, sort=None):
          """
          Form the union of two Index objects.
  
          If the Index objects are incompatible, both Index objects will be
          cast to dtype('object') first.
  
              .. versionchanged:: 0.25.0
  
          Parameters
          ----------
          other : Index or array-like
          sort : bool or None, default None
              Whether to sort the resulting Index.
  
              * None : Sort the result, except when
  
                1. `self` and `other` are equal.
                2. `self` or `other` has length 0.
                3. Some values in `self` or `other` cannot be compared.
                   A RuntimeWarning is issued in this case.
  
              * False : do not sort the result.
  
          Returns
          -------
          union : Index
  
          Examples
          --------
          Union matching dtypes
  
          >>> idx1 = pd.Index([1, 2, 3, 4])
          >>> idx2 = pd.Index([3, 4, 5, 6])
          >>> idx1.union(idx2)
          Int64Index([1, 2, 3, 4, 5, 6], dtype='int64')
  
          Union mismatched dtypes
  
          >>> idx1 = pd.Index(['a', 'b', 'c', 'd'])
          >>> idx2 = pd.Index([1, 2, 3, 4])
          >>> idx1.union(idx2)
          Index(['a', 'b', 'c', 'd', 1, 2, 3, 4], dtype='object')
  
          MultiIndex case
  
          >>> idx1 = pd.MultiIndex.from_arrays(
          ...     [[1, 1, 2, 2], ["Red", "Blue", "Red", "Blue"]]
          ... )
          >>> idx1
          MultiIndex([(1,  'Red'),
              (1, 'Blue'),
              (2,  'Red'),
              (2, 'Blue')],
             )
          >>> idx2 = pd.MultiIndex.from_arrays(
          ...     [[3, 3, 2, 2], ["Red", "Green", "Red", "Green"]]
          ... )
          >>> idx2
          MultiIndex([(3,   'Red'),
              (3, 'Green'),
              (2,   'Red'),
              (2, 'Green')],
             )
          >>> idx1.union(idx2)
          MultiIndex([(1,  'Blue'),
              (1,   'Red'),
              (2,  'Blue'),
              (2, 'Green'),
              (2,   'Red'),
              (3, 'Green'),
              (3,   'Red')],
             )
          >>> idx1.union(idx2, sort=False)
          MultiIndex([(1,   'Red'),
              (1,  'Blue'),
              (2,   'Red'),
              (2,  'Blue'),
              (3,   'Red'),
              (3, 'Green'),
              (2, 'Green')],
             )
          """
          self._validate_sort_keyword(sort)
          self._assert_can_do_setop(other)
          other, result_name = self._convert_can_do_setop(other)
  
          if not is_dtype_equal(self.dtype, other.dtype):
              if (
                  isinstance(self, ABCMultiIndex)
                  and not is_object_dtype(unpack_nested_dtype(other))
                  and len(other) > 0
              ):
                  raise NotImplementedError(
                      "Can only union MultiIndex with MultiIndex or Index of tuples, "
                      "try mi.to_flat_index().union(other) instead."
                  )
              if (
                  isinstance(self, ABCDatetimeIndex)
                  and isinstance(other, ABCDatetimeIndex)
                  and self.tz is not None
                  and other.tz is not None
              ):
                  # GH#39328
                  warnings.warn(
                      "In a future version, the union of DatetimeIndex objects "
                      "with mismatched timezones will cast both to UTC instead of "
                      "object dtype. To retain the old behavior, "
                      "use `index.astype(object).union(other)`",
                      FutureWarning,
                      stacklevel=find_stack_level(),
                  )
  
              dtype = self._find_common_type_compat(other)
              left = self.astype(dtype, copy=False)
              right = other.astype(dtype, copy=False)
              return left.union(right, sort=sort)
  
          elif not len(other) or self.equals(other):
              # NB: whether this (and the `if not len(self)` check below) come before
              #  or after the is_dtype_equal check above affects the returned dtype
              return self._get_reconciled_name_object(other)
  
          elif not len(self):
              return other._get_reconciled_name_object(self)
  
          result = self._union(other, sort=sort)
  
          return self._wrap_setop_result(other, result)
  
      def _union(self, other: Index, sort):
          """
          Specific union logic should go here. In subclasses, union behavior
          should be overwritten here rather than in `self.union`.
  
          Parameters
          ----------
          other : Index or array-like
          sort : False or None, default False
              Whether to sort the resulting index.
  
              * False : do not sort the result.
              * None : sort the result, except when `self` and `other` are equal
                or when the values cannot be compared.
  
          Returns
          -------
          Index
          """
          # TODO(EA): setops-refactor, clean all this up
          lvals = self._values
          rvals = other._values
  
          if (
              sort is None
              and self.is_monotonic
              and other.is_monotonic
              and not (self.has_duplicates and other.has_duplicates)
              and self._can_use_libjoin
          ):
              # Both are monotonic and at least one is unique, so can use outer join
              #  (actually don't need either unique, but without this restriction
              #  test_union_same_value_duplicated_in_both fails)
              try:
                  return self._outer_indexer(other)[0]
              except (TypeError, IncompatibleFrequency):
                  # incomparable objects; should only be for object dtype
                  value_list = list(lvals)
  
                  # worth making this faster? a very unusual case
                  value_set = set(lvals)
                  value_list.extend([x for x in rvals if x not in value_set])
                  # If objects are unorderable, we must have object dtype.
                  return np.array(value_list, dtype=object)
  
          elif not other.is_unique:
              # other has duplicates
              result = algos.union_with_duplicates(lvals, rvals)
              return _maybe_try_sort(result, sort)
  
          # Self may have duplicates; other already checked as unique
          # find indexes of things in "other" that are not in "self"
          if self._index_as_unique:
              indexer = self.get_indexer(other)
              missing = (indexer == -1).nonzero()[0]
          else:
              missing = algos.unique1d(self.get_indexer_non_unique(other)[1])
  
          if len(missing) > 0:
              other_diff = rvals.take(missing)
              result = concat_compat((lvals, other_diff))
          else:
              result = lvals
  
          if not self.is_monotonic or not other.is_monotonic:
              # if both are monotonic then result should already be sorted
              result = _maybe_try_sort(result, sort)
  
          return result
  
      @final
      def _wrap_setop_result(self, other: Index, result) -> Index:
          name = get_op_result_name(self, other)
          if isinstance(result, Index):
              if result.name != name:
                  return result.rename(name)
              return result
          else:
              return self._shallow_copy(result, name=name)
  
      # TODO: standardize return type of non-union setops type(self vs other)
      @final
      def intersection(self, other, sort=False):
          """
          Form the intersection of two Index objects.
  
          This returns a new Index with elements common to the index and `other`.
  
          Parameters
          ----------
          other : Index or array-like
          sort : False or None, default False
              Whether to sort the resulting index.
  
              * False : do not sort the result.
              * None : sort the result, except when `self` and `other` are equal
                or when the values cannot be compared.
  
          Returns
          -------
          intersection : Index
  
          Examples
          --------
          >>> idx1 = pd.Index([1, 2, 3, 4])
          >>> idx2 = pd.Index([3, 4, 5, 6])
          >>> idx1.intersection(idx2)
          Int64Index([3, 4], dtype='int64')
          """
          self._validate_sort_keyword(sort)
          self._assert_can_do_setop(other)
          other, result_name = self._convert_can_do_setop(other)
  
          if self.equals(other):
              if self.has_duplicates:
                  return self.unique()._get_reconciled_name_object(other)
              return self._get_reconciled_name_object(other)
  
          if len(self) == 0 or len(other) == 0:
              # fastpath; we need to be careful about having commutativity
  
              if self._is_multi or other._is_multi:
                  # _convert_can_do_setop ensures that we have both or neither
                  # We retain self.levels
                  return self[:0].rename(result_name)
  
              dtype = self._find_common_type_compat(other)
              if is_dtype_equal(self.dtype, dtype):
                  # Slicing allows us to retain DTI/TDI.freq, RangeIndex
  
                  # Note: self[:0] vs other[:0] affects
                  #  1) which index's `freq` we get in DTI/TDI cases
                  #     This may be a historical artifact, i.e. no documented
                  #     reason for this choice.
                  #  2) The `step` we get in RangeIndex cases
                  if len(self) == 0:
                      return self[:0].rename(result_name)
                  else:
                      return other[:0].rename(result_name)
  
              return Index([], dtype=dtype, name=result_name)
  
          elif not self._should_compare(other):
              # We can infer that the intersection is empty.
              if isinstance(self, ABCMultiIndex):
                  return self[:0].rename(result_name)
              return Index([], name=result_name)
  
          elif not is_dtype_equal(self.dtype, other.dtype):
              dtype = self._find_common_type_compat(other)
              this = self.astype(dtype, copy=False)
              other = other.astype(dtype, copy=False)
              return this.intersection(other, sort=sort)
  
          result = self._intersection(other, sort=sort)
          return self._wrap_intersection_result(other, result)
  
      def _intersection(self, other: Index, sort=False):
          """
          intersection specialized to the case with matching dtypes.
          """
          if self.is_monotonic and other.is_monotonic and self._can_use_libjoin:
              try:
                  result = self._inner_indexer(other)[0]
              except TypeError:
                  # non-comparable; should only be for object dtype
                  pass
              else:
                  # TODO: algos.unique1d should preserve DTA/TDA
                  res = algos.unique1d(result)
                  return ensure_wrapped_if_datetimelike(res)
  
          res_values = self._intersection_via_get_indexer(other, sort=sort)
          res_values = _maybe_try_sort(res_values, sort)
          return res_values
  
      def _wrap_intersection_result(self, other, result):
          # We will override for MultiIndex to handle empty results
          return self._wrap_setop_result(other, result)
  
      @final
      def _intersection_via_get_indexer(self, other: Index, sort) -> ArrayLike:
          """
          Find the intersection of two Indexes using get_indexer.
  
          Returns
          -------
          np.ndarray or ExtensionArray
              The returned array will be unique.
          """
          left_unique = self.unique()
          right_unique = other.unique()
  
          # even though we are unique, we need get_indexer_for for IntervalIndex
          indexer = left_unique.get_indexer_for(right_unique)
  
          mask = indexer != -1
  
          taker = indexer.take(mask.nonzero()[0])
          if sort is False:
              # sort bc we want the elements in the same order they are in self
              # unnecessary in the case with sort=None bc we will sort later
              taker = np.sort(taker)
  
          result = left_unique.take(taker)._values
          return result
  
      @final
      def difference(self, other, sort=None):
          """
          Return a new Index with elements of index not in `other`.
  
          This is the set difference of two Index objects.
  
          Parameters
          ----------
          other : Index or array-like
          sort : False or None, default None
              Whether to sort the resulting index. By default, the
              values are attempted to be sorted, but any TypeError from
              incomparable elements is caught by pandas.
  
              * None : Attempt to sort the result, but catch any TypeErrors
                from comparing incomparable elements.
              * False : Do not sort the result.
  
          Returns
          -------
          difference : Index
  
          Examples
          --------
          >>> idx1 = pd.Index([2, 1, 3, 4])
          >>> idx2 = pd.Index([3, 4, 5, 6])
          >>> idx1.difference(idx2)
          Int64Index([1, 2], dtype='int64')
          >>> idx1.difference(idx2, sort=False)
          Int64Index([2, 1], dtype='int64')
          """
          self._validate_sort_keyword(sort)
          self._assert_can_do_setop(other)
          other, result_name = self._convert_can_do_setop(other)
  
          if self.equals(other):
              # Note: we do not (yet) sort even if sort=None GH#24959
              return self[:0].rename(result_name)
  
          if len(other) == 0:
              # Note: we do not (yet) sort even if sort=None GH#24959
              return self.rename(result_name)
  
          if not self._should_compare(other):
              # Nothing matches -> difference is everything
              return self.rename(result_name)
  
          result = self._difference(other, sort=sort)
          return self._wrap_difference_result(other, result)
  
      def _difference(self, other, sort):
          # overridden by RangeIndex
  
          this = self.unique()
  
          indexer = this.get_indexer_for(other)
          indexer = indexer.take((indexer != -1).nonzero()[0])
  
          label_diff = np.setdiff1d(np.arange(this.size), indexer, assume_unique=True)
          the_diff = this._values.take(label_diff)
          the_diff = _maybe_try_sort(the_diff, sort)
  
          return the_diff
  
      def _wrap_difference_result(self, other, result):
          # We will override for MultiIndex to handle empty results
          return self._wrap_setop_result(other, result)
  
      def symmetric_difference(self, other, result_name=None, sort=None):
          """
          Compute the symmetric difference of two Index objects.
  
          Parameters
          ----------
          other : Index or array-like
          result_name : str
          sort : False or None, default None
              Whether to sort the resulting index. By default, the
              values are attempted to be sorted, but any TypeError from
              incomparable elements is caught by pandas.
  
              * None : Attempt to sort the result, but catch any TypeErrors
                from comparing incomparable elements.
              * False : Do not sort the result.
  
          Returns
          -------
          symmetric_difference : Index
  
          Notes
          -----
          ``symmetric_difference`` contains elements that appear in either
          ``idx1`` or ``idx2`` but not both. Equivalent to the Index created by
          ``idx1.difference(idx2) | idx2.difference(idx1)`` with duplicates
          dropped.
  
          Examples
          --------
          >>> idx1 = pd.Index([1, 2, 3, 4])
          >>> idx2 = pd.Index([2, 3, 4, 5])
          >>> idx1.symmetric_difference(idx2)
          Int64Index([1, 5], dtype='int64')
          """
          self._validate_sort_keyword(sort)
          self._assert_can_do_setop(other)
          other, result_name_update = self._convert_can_do_setop(other)
          if result_name is None:
              result_name = result_name_update
  
          if not self._should_compare(other):
              return self.union(other, sort=sort).rename(result_name)
  
          elif not is_dtype_equal(self.dtype, other.dtype):
              dtype = self._find_common_type_compat(other)
              this = self.astype(dtype, copy=False)
              that = other.astype(dtype, copy=False)
              return this.symmetric_difference(that, sort=sort).rename(result_name)
  
          this = self.unique()
          other = other.unique()
          indexer = this.get_indexer_for(other)
  
          # {this} minus {other}
          common_indexer = indexer.take((indexer != -1).nonzero()[0])
          left_indexer = np.setdiff1d(
              np.arange(this.size), common_indexer, assume_unique=True
          )
          left_diff = this._values.take(left_indexer)
  
          # {other} minus {this}
          right_indexer = (indexer == -1).nonzero()[0]
          right_diff = other._values.take(right_indexer)
  
          res_values = concat_compat([left_diff, right_diff])
          res_values = _maybe_try_sort(res_values, sort)
  
          result = Index(res_values, name=result_name)
  
          if self._is_multi:
              self = cast("MultiIndex", self)
              if len(result) == 0:
                  # On equal symmetric_difference MultiIndexes the difference is empty.
                  # Therefore, an empty MultiIndex is returned GH#13490
                  return type(self)(
                      levels=[[] for _ in range(self.nlevels)],
                      codes=[[] for _ in range(self.nlevels)],
                      names=result.name,
                  )
              return type(self).from_tuples(result, names=result.name)
  
          return result
  
      @final
      def _assert_can_do_setop(self, other) -> bool:
          if not is_list_like(other):
              raise TypeError("Input must be Index or array-like")
          return True
  
      def _convert_can_do_setop(self, other) -> tuple[Index, Hashable]:
          if not isinstance(other, Index):
              other = Index(other, name=self.name)
              result_name = self.name
          else:
              result_name = get_op_result_name(self, other)
          return other, result_name
  
      # --------------------------------------------------------------------
      # Indexing Methods
  
      def get_loc(self, key, method=None, tolerance=None):
          """
          Get integer location, slice or boolean mask for requested label.
  
          Parameters
          ----------
          key : label
          method : {None, 'pad'/'ffill', 'backfill'/'bfill', 'nearest'}, optional
              * default: exact matches only.
              * pad / ffill: find the PREVIOUS index value if no exact match.
              * backfill / bfill: use NEXT index value if no exact match
              * nearest: use the NEAREST index value if no exact match. Tied
                distances are broken by preferring the larger index value.
          tolerance : int or float, optional
              Maximum distance from index value for inexact matches. The value of
              the index at the matching location must satisfy the equation
              ``abs(index[loc] - key) <= tolerance``.
  
          Returns
          -------
          loc : int if unique index, slice if monotonic index, else mask
  
          Examples
          --------
          >>> unique_index = pd.Index(list('abc'))
          >>> unique_index.get_loc('b')
          1
  
          >>> monotonic_index = pd.Index(list('abbc'))
          >>> monotonic_index.get_loc('b')
          slice(1, 3, None)
  
          >>> non_monotonic_index = pd.Index(list('abcb'))
          >>> non_monotonic_index.get_loc('b')
          array([False,  True, False,  True])
          """
          if method is None:
              if tolerance is not None:
                  raise ValueError(
                      "tolerance argument only valid if using pad, "
                      "backfill or nearest lookups"
                  )
              casted_key = self._maybe_cast_indexer(key)
              try:
                  return self._engine.get_loc(casted_key)
              except KeyError as err:
                  raise KeyError(key) from err
  
          # GH#42269
          warnings.warn(
              f"Passing method to {type(self).__name__}.get_loc is deprecated "
              "and will raise in a future version. Use "
              "index.get_indexer([item], method=...) instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
  
          if is_scalar(key) and isna(key) and not self.hasnans:
              raise KeyError(key)
  
          if tolerance is not None:
              tolerance = self._convert_tolerance(tolerance, np.asarray(key))
  
          indexer = self.get_indexer([key], method=method, tolerance=tolerance)
          if indexer.ndim > 1 or indexer.size > 1:
              raise TypeError("get_loc requires scalar valued input")
          loc = indexer.item()
          if loc == -1:
              raise KeyError(key)
          return loc
  
      _index_shared_docs[
          "get_indexer"
      ] = """
          Compute indexer and mask for new index given the current index. The
          indexer should be then used as an input to ndarray.take to align the
          current data to the new index.
  
          Parameters
          ----------
          target : %(target_klass)s
          method : {None, 'pad'/'ffill', 'backfill'/'bfill', 'nearest'}, optional
              * default: exact matches only.
              * pad / ffill: find the PREVIOUS index value if no exact match.
              * backfill / bfill: use NEXT index value if no exact match
              * nearest: use the NEAREST index value if no exact match. Tied
                distances are broken by preferring the larger index value.
          limit : int, optional
              Maximum number of consecutive labels in ``target`` to match for
              inexact matches.
          tolerance : optional
              Maximum distance between original and new labels for inexact
              matches. The values of the index at the matching locations must
              satisfy the equation ``abs(index[indexer] - target) <= tolerance``.
  
              Tolerance may be a scalar value, which applies the same tolerance
              to all values, or list-like, which applies variable tolerance per
              element. List-like includes list, tuple, array, Series, and must be
              the same size as the index and its dtype must exactly match the
              index's type.
  
          Returns
          -------
          indexer : np.ndarray[np.intp]
              Integers from 0 to n - 1 indicating that the index at these
              positions matches the corresponding target values. Missing values
              in the target are marked by -1.
          %(raises_section)s
          Notes
          -----
          Returns -1 for unmatched values, for further explanation see the
          example below.
  
          Examples
          --------
          >>> index = pd.Index(['c', 'a', 'b'])
          >>> index.get_indexer(['a', 'b', 'x'])
          array([ 1,  2, -1])
  
          Notice that the return value is an array of locations in ``index``
          and ``x`` is marked by -1, as it is not in ``index``.
          """
  
      @Appender(_index_shared_docs["get_indexer"] % _index_doc_kwargs)
      @final
      def get_indexer(
          self,
          target,
          method: str_t | None = None,
          limit: int | None = None,
          tolerance=None,
      ) -> npt.NDArray[np.intp]:
          method = missing.clean_reindex_fill_method(method)
          target = self._maybe_cast_listlike_indexer(target)
  
          self._check_indexing_method(method, limit, tolerance)
  
          if not self._index_as_unique:
              raise InvalidIndexError(self._requires_unique_msg)
  
          if len(target) == 0:
              return np.array([], dtype=np.intp)
  
          if not self._should_compare(target) and not self._should_partial_index(target):
              # IntervalIndex get special treatment bc numeric scalars can be
              #  matched to Interval scalars
              return self._get_indexer_non_comparable(target, method=method, unique=True)
  
          if is_categorical_dtype(self.dtype):
              # _maybe_cast_listlike_indexer ensures target has our dtype
              #  (could improve perf by doing _should_compare check earlier?)
              assert is_dtype_equal(self.dtype, target.dtype)
  
              indexer = self._engine.get_indexer(target.codes)
              if self.hasnans and target.hasnans:
                  loc = self.get_loc(np.nan)
                  mask = target.isna()
                  indexer[mask] = loc
              return indexer
  
          if is_categorical_dtype(target.dtype):
              # potential fastpath
              # get an indexer for unique categories then propagate to codes via take_nd
              # get_indexer instead of _get_indexer needed for MultiIndex cases
              #  e.g. test_append_different_columns_types
              categories_indexer = self.get_indexer(target.categories)
  
              indexer = algos.take_nd(categories_indexer, target.codes, fill_value=-1)
  
              if (not self._is_multi and self.hasnans) and target.hasnans:
                  # Exclude MultiIndex because hasnans raises NotImplementedError
                  # we should only get here if we are unique, so loc is an integer
                  # GH#41934
                  loc = self.get_loc(np.nan)
                  mask = target.isna()
                  indexer[mask] = loc
  
              return ensure_platform_int(indexer)
  
          pself, ptarget = self._maybe_promote(target)
          if pself is not self or ptarget is not target:
              return pself.get_indexer(
                  ptarget, method=method, limit=limit, tolerance=tolerance
              )
  
          if is_dtype_equal(self.dtype, target.dtype) and self.equals(target):
              # Only call equals if we have same dtype to avoid inference/casting
              return np.arange(len(target), dtype=np.intp)
  
          if not is_dtype_equal(self.dtype, target.dtype) and not is_interval_dtype(
              self.dtype
          ):
              # IntervalIndex gets special treatment for partial-indexing
              dtype = self._find_common_type_compat(target)
  
              this = self.astype(dtype, copy=False)
              target = target.astype(dtype, copy=False)
              return this._get_indexer(
                  target, method=method, limit=limit, tolerance=tolerance
              )
  
          return self._get_indexer(target, method, limit, tolerance)
  
      def _get_indexer(
          self,
          target: Index,
          method: str_t | None = None,
          limit: int | None = None,
          tolerance=None,
      ) -> npt.NDArray[np.intp]:
          if tolerance is not None:
              tolerance = self._convert_tolerance(tolerance, target)
  
          if method in ["pad", "backfill"]:
              indexer = self._get_fill_indexer(target, method, limit, tolerance)
          elif method == "nearest":
              indexer = self._get_nearest_indexer(target, limit, tolerance)
          else:
              tgt_values = target._get_engine_target()
              if target._is_multi and self._is_multi:
                  engine = self._engine
                  # error: "IndexEngine" has no attribute "_extract_level_codes"
                  tgt_values = engine._extract_level_codes(  # type: ignore[attr-defined]
                      target
                  )
  
              indexer = self._engine.get_indexer(tgt_values)
  
          return ensure_platform_int(indexer)
  
      @final
      def _should_partial_index(self, target: Index) -> bool:
          """
          Should we attempt partial-matching indexing?
          """
          if is_interval_dtype(self.dtype):
              # "Index" has no attribute "left"
              return self.left._should_compare(target)  # type: ignore[attr-defined]
          return False
  
      @final
      def _check_indexing_method(
          self,
          method: str_t | None,
          limit: int | None = None,
          tolerance=None,
      ) -> None:
          """
          Raise if we have a get_indexer `method` that is not supported or valid.
          """
          if method not in [None, "bfill", "backfill", "pad", "ffill", "nearest"]:
              # in practice the clean_reindex_fill_method call would raise
              #  before we get here
              raise ValueError("Invalid fill method")  # pragma: no cover
  
          if self._is_multi:
              if method == "nearest":
                  raise NotImplementedError(
                      "method='nearest' not implemented yet "
                      "for MultiIndex; see GitHub issue 9365"
                  )
              elif method == "pad" or method == "backfill":
                  if tolerance is not None:
                      raise NotImplementedError(
                          "tolerance not implemented yet for MultiIndex"
                      )
  
          if is_interval_dtype(self.dtype) or is_categorical_dtype(self.dtype):
              # GH#37871 for now this is only for IntervalIndex and CategoricalIndex
              if method is not None:
                  raise NotImplementedError(
                      f"method {method} not yet implemented for {type(self).__name__}"
                  )
  
          if method is None:
              if tolerance is not None:
                  raise ValueError(
                      "tolerance argument only valid if doing pad, "
                      "backfill or nearest reindexing"
                  )
              if limit is not None:
                  raise ValueError(
                      "limit argument only valid if doing pad, "
                      "backfill or nearest reindexing"
                  )
  
      def _convert_tolerance(self, tolerance, target: np.ndarray | Index) -> np.ndarray:
          # override this method on subclasses
          tolerance = np.asarray(tolerance)
          if target.size != tolerance.size and tolerance.size > 1:
              raise ValueError("list-like tolerance size must match target index size")
          return tolerance
  
      @final
      def _get_fill_indexer(
          self, target: Index, method: str_t, limit: int | None = None, tolerance=None
      ) -> npt.NDArray[np.intp]:
  
          if self._is_multi:
              # TODO: get_indexer_with_fill docstring says values must be _sorted_
              #  but that doesn't appear to be enforced
              # error: "IndexEngine" has no attribute "get_indexer_with_fill"
              return self._engine.get_indexer_with_fill(  # type: ignore[attr-defined]
                  target=target._values, values=self._values, method=method, limit=limit
              )
  
          if self.is_monotonic_increasing and target.is_monotonic_increasing:
              target_values = target._get_engine_target()
              own_values = self._get_engine_target()
  
              if method == "pad":
                  indexer = libalgos.pad(own_values, target_values, limit=limit)
              else:
                  # i.e. "backfill"
                  indexer = libalgos.backfill(own_values, target_values, limit=limit)
          else:
              indexer = self._get_fill_indexer_searchsorted(target, method, limit)
          if tolerance is not None and len(self):
              indexer = self._filter_indexer_tolerance(target, indexer, tolerance)
          return indexer
  
      @final
      def _get_fill_indexer_searchsorted(
          self, target: Index, method: str_t, limit: int | None = None
      ) -> npt.NDArray[np.intp]:
          """
          Fallback pad/backfill get_indexer that works for monotonic decreasing
          indexes and non-monotonic targets.
          """
          if limit is not None:
              raise ValueError(
                  f"limit argument for {repr(method)} method only well-defined "
                  "if index and target are monotonic"
              )
  
          side: Literal["left", "right"] = "left" if method == "pad" else "right"
  
          # find exact matches first (this simplifies the algorithm)
          indexer = self.get_indexer(target)
          nonexact = indexer == -1
          indexer[nonexact] = self._searchsorted_monotonic(target[nonexact], side)
          if side == "left":
              # searchsorted returns "indices into a sorted array such that,
              # if the corresponding elements in v were inserted before the
              # indices, the order of a would be preserved".
              # Thus, we need to subtract 1 to find values to the left.
              indexer[nonexact] -= 1
              # This also mapped not found values (values of 0 from
              # np.searchsorted) to -1, which conveniently is also our
              # sentinel for missing values
          else:
              # Mark indices to the right of the largest value as not found
              indexer[indexer == len(self)] = -1
          return indexer
  
      @final
      def _get_nearest_indexer(
          self, target: Index, limit: int | None, tolerance
      ) -> npt.NDArray[np.intp]:
          """
          Get the indexer for the nearest index labels; requires an index with
          values that can be subtracted from each other (e.g., not strings or
          tuples).
          """
          if not len(self):
              return self._get_fill_indexer(target, "pad")
  
          left_indexer = self.get_indexer(target, "pad", limit=limit)
          right_indexer = self.get_indexer(target, "backfill", limit=limit)
  
          left_distances = self._difference_compat(target, left_indexer)
          right_distances = self._difference_compat(target, right_indexer)
  
          op = operator.lt if self.is_monotonic_increasing else operator.le
          indexer = np.where(
              op(left_distances, right_distances) | (right_indexer == -1),
              left_indexer,
              right_indexer,
          )
          if tolerance is not None:
              indexer = self._filter_indexer_tolerance(target, indexer, tolerance)
          return indexer
  
      @final
      def _filter_indexer_tolerance(
          self,
          target: Index,
          indexer: npt.NDArray[np.intp],
          tolerance,
      ) -> npt.NDArray[np.intp]:
  
          distance = self._difference_compat(target, indexer)
  
          return np.where(distance <= tolerance, indexer, -1)
  
      @final
      def _difference_compat(
          self, target: Index, indexer: npt.NDArray[np.intp]
      ) -> ArrayLike:
          # Compatibility for PeriodArray, for which __sub__ returns an ndarray[object]
          #  of DateOffset objects, which do not support __abs__ (and would be slow
          #  if they did)
  
          if isinstance(self.dtype, PeriodDtype):
              # Note: we only get here with matching dtypes
              own_values = cast("PeriodArray", self._data)._ndarray
              target_values = cast("PeriodArray", target._data)._ndarray
              diff = own_values[indexer] - target_values
          else:
              # error: Unsupported left operand type for - ("ExtensionArray")
              diff = self._values[indexer] - target._values  # type: ignore[operator]
          return abs(diff)
  
      # --------------------------------------------------------------------
      # Indexer Conversion Methods
  
      @final
      def _validate_positional_slice(self, key: slice) -> None:
          """
          For positional indexing, a slice must have either int or None
          for each of start, stop, and step.
          """
          self._validate_indexer("positional", key.start, "iloc")
          self._validate_indexer("positional", key.stop, "iloc")
          self._validate_indexer("positional", key.step, "iloc")
  
      def _convert_slice_indexer(self, key: slice, kind: str_t):
          """
          Convert a slice indexer.
  
          By definition, these are labels unless 'iloc' is passed in.
          Floats are not allowed as the start, step, or stop of the slice.
  
          Parameters
          ----------
          key : label of the slice bound
          kind : {'loc', 'getitem'}
          """
          assert kind in ["loc", "getitem"], kind
  
          # potentially cast the bounds to integers
          start, stop, step = key.start, key.stop, key.step
  
          # figure out if this is a positional indexer
          def is_int(v):
              return v is None or is_integer(v)
  
          is_index_slice = is_int(start) and is_int(stop) and is_int(step)
          is_positional = is_index_slice and not (
              self.is_integer() or self.is_categorical()
          )
  
          if kind == "getitem":
              """
              called from the getitem slicers, validate that we are in fact
              integers
              """
              if self.is_integer() or is_index_slice:
                  self._validate_indexer("slice", key.start, "getitem")
                  self._validate_indexer("slice", key.stop, "getitem")
                  self._validate_indexer("slice", key.step, "getitem")
                  return key
  
          # convert the slice to an indexer here
  
          # if we are mixed and have integers
          if is_positional:
              try:
                  # Validate start & stop
                  if start is not None:
                      self.get_loc(start)
                  if stop is not None:
                      self.get_loc(stop)
                  is_positional = False
              except KeyError:
                  pass
  
          if com.is_null_slice(key):
              # It doesn't matter if we are positional or label based
              indexer = key
          elif is_positional:
              if kind == "loc":
                  # GH#16121, GH#24612, GH#31810
                  warnings.warn(
                      "Slicing a positional slice with .loc is not supported, "
                      "and will raise TypeError in a future version.  "
                      "Use .loc with labels or .iloc with positions instead.",
                      FutureWarning,
                      stacklevel=find_stack_level(),
                  )
              indexer = key
          else:
              indexer = self.slice_indexer(start, stop, step)
  
          return indexer
  
      @final
      def _invalid_indexer(self, form: str_t, key) -> TypeError:
          """
          Consistent invalid indexer message.
          """
          return TypeError(
              f"cannot do {form} indexing on {type(self).__name__} with these "
              f"indexers [{key}] of type {type(key).__name__}"
          )
  
      # --------------------------------------------------------------------
      # Reindex Methods
  
      @final
      def _validate_can_reindex(self, indexer: np.ndarray) -> None:
          """
          Check if we are allowing reindexing with this particular indexer.
  
          Parameters
          ----------
          indexer : an integer ndarray
  
          Raises
          ------
          ValueError if its a duplicate axis
          """
          # trying to reindex on an axis with duplicates
          if not self._index_as_unique and len(indexer):
              raise ValueError("cannot reindex on an axis with duplicate labels")
  
      def reindex(
          self, target, method=None, level=None, limit=None, tolerance=None
      ) -> tuple[Index, npt.NDArray[np.intp] | None]:
          """
          Create index with target's values.
  
          Parameters
          ----------
          target : an iterable
          method : {None, 'pad'/'ffill', 'backfill'/'bfill', 'nearest'}, optional
              * default: exact matches only.
              * pad / ffill: find the PREVIOUS index value if no exact match.
              * backfill / bfill: use NEXT index value if no exact match
              * nearest: use the NEAREST index value if no exact match. Tied
                distances are broken by preferring the larger index value.
          level : int, optional
              Level of multiindex.
          limit : int, optional
              Maximum number of consecutive labels in ``target`` to match for
              inexact matches.
          tolerance : int or float, optional
              Maximum distance between original and new labels for inexact
              matches. The values of the index at the matching locations must
              satisfy the equation ``abs(index[indexer] - target) <= tolerance``.
  
              Tolerance may be a scalar value, which applies the same tolerance
              to all values, or list-like, which applies variable tolerance per
              element. List-like includes list, tuple, array, Series, and must be
              the same size as the index and its dtype must exactly match the
              index's type.
  
          Returns
          -------
          new_index : pd.Index
              Resulting index.
          indexer : np.ndarray[np.intp] or None
              Indices of output values in original index.
  
          Raises
          ------
          TypeError
              If ``method`` passed along with ``level``.
          ValueError
              If non-unique multi-index
          ValueError
              If non-unique index and ``method`` or ``limit`` passed.
  
          See Also
          --------
          Series.reindex
          DataFrame.reindex
  
          Examples
          --------
          >>> idx = pd.Index(['car', 'bike', 'train', 'tractor'])
          >>> idx
          Index(['car', 'bike', 'train', 'tractor'], dtype='object')
          >>> idx.reindex(['car', 'bike'])
          (Index(['car', 'bike'], dtype='object'), array([0, 1]))
          """
          # GH6552: preserve names when reindexing to non-named target
          # (i.e. neither Index nor Series).
          preserve_names = not hasattr(target, "name")
  
          # GH7774: preserve dtype/tz if target is empty and not an Index.
          target = ensure_has_len(target)  # target may be an iterator
  
          if not isinstance(target, Index) and len(target) == 0:
              if level is not None and self._is_multi:
                  # "Index" has no attribute "levels"; maybe "nlevels"?
                  idx = self.levels[level]  # type: ignore[attr-defined]
              else:
                  idx = self
              target = idx[:0]
          else:
              target = ensure_index(target)
  
          if level is not None:
              if method is not None:
                  raise TypeError("Fill method not supported if level passed")
  
              # TODO: tests where passing `keep_order=not self._is_multi`
              #  makes a difference for non-MultiIndex case
              target, indexer, _ = self._join_level(
                  target, level, how="right", keep_order=not self._is_multi
              )
  
          else:
              if self.equals(target):
                  indexer = None
              else:
                  if self._index_as_unique:
                      indexer = self.get_indexer(
                          target, method=method, limit=limit, tolerance=tolerance
                      )
                  elif self._is_multi:
                      raise ValueError("cannot handle a non-unique multi-index!")
                  else:
                      if method is not None or limit is not None:
                          raise ValueError(
                              "cannot reindex a non-unique index "
                              "with a method or limit"
                          )
                      indexer, _ = self.get_indexer_non_unique(target)
  
                  if not self.is_unique:
                      # GH#42568
                      warnings.warn(
                          "reindexing with a non-unique Index is deprecated and "
                          "will raise in a future version.",
                          FutureWarning,
                          stacklevel=find_stack_level(),
                      )
  
          target = self._wrap_reindex_result(target, indexer, preserve_names)
          return target, indexer
  
      def _wrap_reindex_result(self, target, indexer, preserve_names: bool):
          target = self._maybe_preserve_names(target, preserve_names)
          return target
  
      def _maybe_preserve_names(self, target: Index, preserve_names: bool):
          if preserve_names and target.nlevels == 1 and target.name != self.name:
              target = target.copy(deep=False)
              target.name = self.name
          return target
  
      @final
      def _reindex_non_unique(
          self, target: Index
      ) -> tuple[Index, npt.NDArray[np.intp], npt.NDArray[np.intp] | None]:
          """
          Create a new index with target's values (move/add/delete values as
          necessary) use with non-unique Index and a possibly non-unique target.
  
          Parameters
          ----------
          target : an iterable
  
          Returns
          -------
          new_index : pd.Index
              Resulting index.
          indexer : np.ndarray[np.intp]
              Indices of output values in original index.
          new_indexer : np.ndarray[np.intp] or None
  
          """
          target = ensure_index(target)
          if len(target) == 0:
              # GH#13691
              return self[:0], np.array([], dtype=np.intp), None
  
          indexer, missing = self.get_indexer_non_unique(target)
          check = indexer != -1
          new_labels = self.take(indexer[check])
          new_indexer = None
  
          if len(missing):
              length = np.arange(len(indexer), dtype=np.intp)
  
              missing = ensure_platform_int(missing)
              missing_labels = target.take(missing)
              missing_indexer = length[~check]
              cur_labels = self.take(indexer[check]).values
              cur_indexer = length[check]
  
              # Index constructor below will do inference
              new_labels = np.empty((len(indexer),), dtype=object)
              new_labels[cur_indexer] = cur_labels
              new_labels[missing_indexer] = missing_labels
  
              # GH#38906
              if not len(self):
  
                  new_indexer = np.arange(0, dtype=np.intp)
  
              # a unique indexer
              elif target.is_unique:
  
                  # see GH5553, make sure we use the right indexer
                  new_indexer = np.arange(len(indexer), dtype=np.intp)
                  new_indexer[cur_indexer] = np.arange(len(cur_labels))
                  new_indexer[missing_indexer] = -1
  
              # we have a non_unique selector, need to use the original
              # indexer here
              else:
  
                  # need to retake to have the same size as the indexer
                  indexer[~check] = -1
  
                  # reset the new indexer to account for the new size
                  new_indexer = np.arange(len(self.take(indexer)), dtype=np.intp)
                  new_indexer[~check] = -1
  
          if isinstance(self, ABCMultiIndex):
              new_index = type(self).from_tuples(new_labels, names=self.names)
          else:
              new_index = Index._with_infer(new_labels, name=self.name)
          return new_index, indexer, new_indexer
  
      # --------------------------------------------------------------------
      # Join Methods
  
      @final
      @_maybe_return_indexers
      def join(
          self,
          other,
          how: str_t = "left",
          level=None,
          return_indexers: bool = False,
          sort: bool = False,
      ):
          """
          Compute join_index and indexers to conform data
          structures to the new index.
  
          Parameters
          ----------
          other : Index
          how : {'left', 'right', 'inner', 'outer'}
          level : int or level name, default None
          return_indexers : bool, default False
          sort : bool, default False
              Sort the join keys lexicographically in the result Index. If False,
              the order of the join keys depends on the join type (how keyword).
  
          Returns
          -------
          join_index, (left_indexer, right_indexer)
          """
          other = ensure_index(other)
  
          if isinstance(self, ABCDatetimeIndex) and isinstance(other, ABCDatetimeIndex):
              if (self.tz is None) ^ (other.tz is None):
                  # Raise instead of casting to object below.
                  raise TypeError("Cannot join tz-naive with tz-aware DatetimeIndex")
  
          if not self._is_multi and not other._is_multi:
              # We have specific handling for MultiIndex below
              pself, pother = self._maybe_promote(other)
              if pself is not self or pother is not other:
                  return pself.join(
                      pother, how=how, level=level, return_indexers=True, sort=sort
                  )
  
          lindexer: np.ndarray | None
          rindexer: np.ndarray | None
  
          # try to figure out the join level
          # GH3662
          if level is None and (self._is_multi or other._is_multi):
  
              # have the same levels/names so a simple join
              if self.names == other.names:
                  pass
              else:
                  return self._join_multi(other, how=how)
  
          # join on the level
          if level is not None and (self._is_multi or other._is_multi):
              return self._join_level(other, level, how=how)
  
          if len(other) == 0 and how in ("left", "outer"):
              join_index = self._view()
              rindexer = np.repeat(np.intp(-1), len(join_index))
              return join_index, None, rindexer
  
          if len(self) == 0 and how in ("right", "outer"):
              join_index = other._view()
              lindexer = np.repeat(np.intp(-1), len(join_index))
              return join_index, lindexer, None
  
          if self._join_precedence < other._join_precedence:
              how = {"right": "left", "left": "right"}.get(how, how)
              join_index, lidx, ridx = other.join(
                  self, how=how, level=level, return_indexers=True
              )
              lidx, ridx = ridx, lidx
              return join_index, lidx, ridx
  
          if not is_dtype_equal(self.dtype, other.dtype):
              dtype = self._find_common_type_compat(other)
              this = self.astype(dtype, copy=False)
              other = other.astype(dtype, copy=False)
              return this.join(other, how=how, return_indexers=True)
  
          _validate_join_method(how)
  
          if not self.is_unique and not other.is_unique:
              return self._join_non_unique(other, how=how)
          elif not self.is_unique or not other.is_unique:
              if self.is_monotonic and other.is_monotonic:
                  if self._can_use_libjoin:
                      # otherwise we will fall through to _join_via_get_indexer
                      return self._join_monotonic(other, how=how)
              else:
                  return self._join_non_unique(other, how=how)
          elif (
              self.is_monotonic
              and other.is_monotonic
              and self._can_use_libjoin
              and (
                  not isinstance(self, ABCMultiIndex)
                  or not any(is_categorical_dtype(dtype) for dtype in self.dtypes)
              )
          ):
              # Categorical is monotonic if data are ordered as categories, but join can
              #  not handle this in case of not lexicographically monotonic GH#38502
              try:
                  return self._join_monotonic(other, how=how)
              except TypeError:
                  # object dtype; non-comparable objects
                  pass
  
          return self._join_via_get_indexer(other, how, sort)
  
      @final
      def _join_via_get_indexer(
          self, other: Index, how: str_t, sort: bool
      ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
          # Fallback if we do not have any fastpaths available based on
          #  uniqueness/monotonicity
  
          # Note: at this point we have checked matching dtypes
  
          if how == "left":
              join_index = self
          elif how == "right":
              join_index = other
          elif how == "inner":
              # TODO: sort=False here for backwards compat. It may
              # be better to use the sort parameter passed into join
              join_index = self.intersection(other, sort=False)
          elif how == "outer":
              # TODO: sort=True here for backwards compat. It may
              # be better to use the sort parameter passed into join
              join_index = self.union(other)
  
          if sort:
              join_index = join_index.sort_values()
  
          if join_index is self:
              lindexer = None
          else:
              lindexer = self.get_indexer(join_index)
          if join_index is other:
              rindexer = None
          else:
              rindexer = other.get_indexer(join_index)
          return join_index, lindexer, rindexer
  
      @final
      def _join_multi(self, other: Index, how: str_t):
          from pandas.core.indexes.multi import MultiIndex
          from pandas.core.reshape.merge import restore_dropped_levels_multijoin
  
          # figure out join names
          self_names_list = list(com.not_none(*self.names))
          other_names_list = list(com.not_none(*other.names))
          self_names_order = self_names_list.index
          other_names_order = other_names_list.index
          self_names = set(self_names_list)
          other_names = set(other_names_list)
          overlap = self_names & other_names
  
          # need at least 1 in common
          if not overlap:
              raise ValueError("cannot join with no overlapping index names")
  
          if isinstance(self, MultiIndex) and isinstance(other, MultiIndex):
  
              # Drop the non-matching levels from left and right respectively
              ldrop_names = sorted(self_names - overlap, key=self_names_order)
              rdrop_names = sorted(other_names - overlap, key=other_names_order)
  
              # if only the order differs
              if not len(ldrop_names + rdrop_names):
                  self_jnlevels = self
                  other_jnlevels = other.reorder_levels(self.names)
              else:
                  self_jnlevels = self.droplevel(ldrop_names)
                  other_jnlevels = other.droplevel(rdrop_names)
  
              # Join left and right
              # Join on same leveled multi-index frames is supported
              join_idx, lidx, ridx = self_jnlevels.join(
                  other_jnlevels, how, return_indexers=True
              )
  
              # Restore the dropped levels
              # Returned index level order is
              # common levels, ldrop_names, rdrop_names
              dropped_names = ldrop_names + rdrop_names
  
              levels, codes, names = restore_dropped_levels_multijoin(
                  self, other, dropped_names, join_idx, lidx, ridx
              )
  
              # Re-create the multi-index
              multi_join_idx = MultiIndex(
                  levels=levels, codes=codes, names=names, verify_integrity=False
              )
  
              multi_join_idx = multi_join_idx.remove_unused_levels()
  
              return multi_join_idx, lidx, ridx
  
          jl = list(overlap)[0]
  
          # Case where only one index is multi
          # make the indices into mi's that match
          flip_order = False
          if isinstance(self, MultiIndex):
              self, other = other, self
              flip_order = True
              # flip if join method is right or left
              how = {"right": "left", "left": "right"}.get(how, how)
  
          level = other.names.index(jl)
          result = self._join_level(other, level, how=how)
  
          if flip_order:
              return result[0], result[2], result[1]
          return result
  
      @final
      def _join_non_unique(
          self, other: Index, how: str_t = "left"
      ) -> tuple[Index, npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          from pandas.core.reshape.merge import get_join_indexers
  
          # We only get here if dtypes match
          assert self.dtype == other.dtype
  
          left_idx, right_idx = get_join_indexers(
              [self._values], [other._values], how=how, sort=True
          )
          mask = left_idx == -1
  
          join_array = self._values.take(left_idx)
          right = other._values.take(right_idx)
  
          if isinstance(join_array, np.ndarray):
              np.putmask(join_array, mask, right)
          else:
              join_array._putmask(mask, right)
  
          join_index = self._wrap_joined_index(join_array, other)
  
          return join_index, left_idx, right_idx
  
      @final
      def _join_level(
          self, other: Index, level, how: str_t = "left", keep_order: bool = True
      ) -> tuple[MultiIndex, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
          """
          The join method *only* affects the level of the resulting
          MultiIndex. Otherwise it just exactly aligns the Index data to the
          labels of the level in the MultiIndex.
  
          If ```keep_order == True```, the order of the data indexed by the
          MultiIndex will not be changed; otherwise, it will tie out
          with `other`.
          """
          from pandas.core.indexes.multi import MultiIndex
  
          def _get_leaf_sorter(labels: list[np.ndarray]) -> npt.NDArray[np.intp]:
              """
              Returns sorter for the inner most level while preserving the
              order of higher levels.
  
              Parameters
              ----------
              labels : list[np.ndarray]
                  Each ndarray has signed integer dtype, not necessarily identical.
  
              Returns
              -------
              np.ndarray[np.intp]
              """
              if labels[0].size == 0:
                  return np.empty(0, dtype=np.intp)
  
              if len(labels) == 1:
                  return get_group_index_sorter(ensure_platform_int(labels[0]))
  
              # find indexers of beginning of each set of
              # same-key labels w.r.t all but last level
              tic = labels[0][:-1] != labels[0][1:]
              for lab in labels[1:-1]:
                  tic |= lab[:-1] != lab[1:]
  
              starts = np.hstack(([True], tic, [True])).nonzero()[0]
              lab = ensure_int64(labels[-1])
              return lib.get_level_sorter(lab, ensure_platform_int(starts))
  
          if isinstance(self, MultiIndex) and isinstance(other, MultiIndex):
              raise TypeError("Join on level between two MultiIndex objects is ambiguous")
  
          left, right = self, other
  
          flip_order = not isinstance(self, MultiIndex)
          if flip_order:
              left, right = right, left
              how = {"right": "left", "left": "right"}.get(how, how)
  
          assert isinstance(left, MultiIndex)
  
          level = left._get_level_number(level)
          old_level = left.levels[level]
  
          if not right.is_unique:
              raise NotImplementedError(
                  "Index._join_level on non-unique index is not implemented"
              )
  
          new_level, left_lev_indexer, right_lev_indexer = old_level.join(
              right, how=how, return_indexers=True
          )
  
          if left_lev_indexer is None:
              if keep_order or len(left) == 0:
                  left_indexer = None
                  join_index = left
              else:  # sort the leaves
                  left_indexer = _get_leaf_sorter(left.codes[: level + 1])
                  join_index = left[left_indexer]
  
          else:
              left_lev_indexer = ensure_platform_int(left_lev_indexer)
              rev_indexer = lib.get_reverse_indexer(left_lev_indexer, len(old_level))
              old_codes = left.codes[level]
  
              taker = old_codes[old_codes != -1]
              new_lev_codes = rev_indexer.take(taker)
  
              new_codes = list(left.codes)
              new_codes[level] = new_lev_codes
  
              new_levels = list(left.levels)
              new_levels[level] = new_level
  
              if keep_order:  # just drop missing values. o.w. keep order
                  left_indexer = np.arange(len(left), dtype=np.intp)
                  left_indexer = cast(np.ndarray, left_indexer)
                  mask = new_lev_codes != -1
                  if not mask.all():
                      new_codes = [lab[mask] for lab in new_codes]
                      left_indexer = left_indexer[mask]
  
              else:  # tie out the order with other
                  if level == 0:  # outer most level, take the fast route
                      max_new_lev = 0 if len(new_lev_codes) == 0 else new_lev_codes.max()
                      ngroups = 1 + max_new_lev
                      left_indexer, counts = libalgos.groupsort_indexer(
                          new_lev_codes, ngroups
                      )
  
                      # missing values are placed first; drop them!
                      left_indexer = left_indexer[counts[0] :]
                      new_codes = [lab[left_indexer] for lab in new_codes]
  
                  else:  # sort the leaves
                      mask = new_lev_codes != -1
                      mask_all = mask.all()
                      if not mask_all:
                          new_codes = [lab[mask] for lab in new_codes]
  
                      left_indexer = _get_leaf_sorter(new_codes[: level + 1])
                      new_codes = [lab[left_indexer] for lab in new_codes]
  
                      # left_indexers are w.r.t masked frame.
                      # reverse to original frame!
                      if not mask_all:
                          left_indexer = mask.nonzero()[0][left_indexer]
  
              join_index = MultiIndex(
                  levels=new_levels,
                  codes=new_codes,
                  names=left.names,
                  verify_integrity=False,
              )
  
          if right_lev_indexer is not None:
              right_indexer = right_lev_indexer.take(join_index.codes[level])
          else:
              right_indexer = join_index.codes[level]
  
          if flip_order:
              left_indexer, right_indexer = right_indexer, left_indexer
  
          left_indexer = (
              None if left_indexer is None else ensure_platform_int(left_indexer)
          )
          right_indexer = (
              None if right_indexer is None else ensure_platform_int(right_indexer)
          )
          return join_index, left_indexer, right_indexer
  
      @final
      def _join_monotonic(
          self, other: Index, how: str_t = "left"
      ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
          # We only get here with matching dtypes and both monotonic increasing
          assert other.dtype == self.dtype
  
          if self.equals(other):
              ret_index = other if how == "right" else self
              return ret_index, None, None
  
          ridx: np.ndarray | None
          lidx: np.ndarray | None
  
          if self.is_unique and other.is_unique:
              # We can perform much better than the general case
              if how == "left":
                  join_index = self
                  lidx = None
                  ridx = self._left_indexer_unique(other)
              elif how == "right":
                  join_index = other
                  lidx = other._left_indexer_unique(self)
                  ridx = None
              elif how == "inner":
                  join_array, lidx, ridx = self._inner_indexer(other)
                  join_index = self._wrap_joined_index(join_array, other)
              elif how == "outer":
                  join_array, lidx, ridx = self._outer_indexer(other)
                  join_index = self._wrap_joined_index(join_array, other)
          else:
              if how == "left":
                  join_array, lidx, ridx = self._left_indexer(other)
              elif how == "right":
                  join_array, ridx, lidx = other._left_indexer(self)
              elif how == "inner":
                  join_array, lidx, ridx = self._inner_indexer(other)
              elif how == "outer":
                  join_array, lidx, ridx = self._outer_indexer(other)
  
              join_index = self._wrap_joined_index(join_array, other)
  
          lidx = None if lidx is None else ensure_platform_int(lidx)
          ridx = None if ridx is None else ensure_platform_int(ridx)
          return join_index, lidx, ridx
  
      def _wrap_joined_index(self: _IndexT, joined: ArrayLike, other: _IndexT) -> _IndexT:
          assert other.dtype == self.dtype
  
          if isinstance(self, ABCMultiIndex):
              name = self.names if self.names == other.names else None
              # error: Incompatible return value type (got "MultiIndex",
              # expected "_IndexT")
              return self._constructor(joined, name=name)  # type: ignore[return-value]
          else:
              name = get_op_result_name(self, other)
              return self._constructor._with_infer(joined, name=name)
  
      @cache_readonly
      def _can_use_libjoin(self) -> bool:
          """
          Whether we can use the fastpaths implement in _libs.join
          """
          # Note: this will need to be updated when e.g. Nullable dtypes
          #  are supported in Indexes.
          return not is_interval_dtype(self.dtype)
  
      # --------------------------------------------------------------------
      # Uncategorized Methods
  
      @property
      def values(self) -> ArrayLike:
          """
          Return an array representing the data in the Index.
  
          .. warning::
  
             We recommend using :attr:`Index.array` or
             :meth:`Index.to_numpy`, depending on whether you need
             a reference to the underlying data or a NumPy array.
  
          Returns
          -------
          array: numpy.ndarray or ExtensionArray
  
          See Also
          --------
          Index.array : Reference to the underlying data.
          Index.to_numpy : A NumPy array representing the underlying data.
          """
          return self._data
  
      # error: Decorated property not supported
      # https://github.com/python/mypy/issues/1362
      @cache_readonly  # type: ignore[misc]
      @doc(IndexOpsMixin.array)
      def array(self) -> ExtensionArray:
          array = self._data
          if isinstance(array, np.ndarray):
              from pandas.core.arrays.numpy_ import PandasArray
  
              array = PandasArray(array)
          return array
  
      @property
      def _values(self) -> ExtensionArray | np.ndarray:
          """
          The best array representation.
  
          This is an ndarray or ExtensionArray.
  
          ``_values`` are consistent between ``Series`` and ``Index``.
  
          It may differ from the public '.values' method.
  
          index             | values          | _values       |
          ----------------- | --------------- | ------------- |
          Index             | ndarray         | ndarray       |
          CategoricalIndex  | Categorical     | Categorical   |
          DatetimeIndex     | ndarray[M8ns]   | DatetimeArray |
          DatetimeIndex[tz] | ndarray[M8ns]   | DatetimeArray |
          PeriodIndex       | ndarray[object] | PeriodArray   |
          IntervalIndex     | IntervalArray   | IntervalArray |
  
          See Also
          --------
          values : Values
          """
          return self._data
  
      def _get_engine_target(self) -> np.ndarray:
          """
          Get the ndarray that we can pass to the IndexEngine constructor.
          """
          # error: Incompatible return value type (got "Union[ExtensionArray,
          # ndarray]", expected "ndarray")
          return self._values  # type: ignore[return-value]
  
      def _from_join_target(self, result: np.ndarray) -> ArrayLike:
          """
          Cast the ndarray returned from one of the libjoin.foo_indexer functions
          back to type(self)._data.
          """
          return result
  
      @doc(IndexOpsMixin._memory_usage)
      def memory_usage(self, deep: bool = False) -> int:
          result = self._memory_usage(deep=deep)
  
          # include our engine hashtable
          result += self._engine.sizeof(deep=deep)
          return result
  
      @final
      def where(self, cond, other=None) -> Index:
          """
          Replace values where the condition is False.
  
          The replacement is taken from other.
  
          Parameters
          ----------
          cond : bool array-like with the same length as self
              Condition to select the values on.
          other : scalar, or array-like, default None
              Replacement if the condition is False.
  
          Returns
          -------
          pandas.Index
              A copy of self with values replaced from other
              where the condition is False.
  
          See Also
          --------
          Series.where : Same method for Series.
          DataFrame.where : Same method for DataFrame.
  
          Examples
          --------
          >>> idx = pd.Index(['car', 'bike', 'train', 'tractor'])
          >>> idx
          Index(['car', 'bike', 'train', 'tractor'], dtype='object')
          >>> idx.where(idx.isin(['car', 'train']), 'other')
          Index(['car', 'other', 'train', 'other'], dtype='object')
          """
          if isinstance(self, ABCMultiIndex):
              raise NotImplementedError(
                  ".where is not supported for MultiIndex operations"
              )
          cond = np.asarray(cond, dtype=bool)
          return self.putmask(~cond, other)
  
      # construction helpers
      @final
      @classmethod
      def _scalar_data_error(cls, data):
          # We return the TypeError so that we can raise it from the constructor
          #  in order to keep mypy happy
          return TypeError(
              f"{cls.__name__}(...) must be called with a collection of some "
              f"kind, {repr(data)} was passed"
          )
  
      @final
      @classmethod
      def _string_data_error(cls, data):
          raise TypeError(
              "String dtype not supported, you may need "
              "to explicitly cast to a numeric type"
          )
  
      def _validate_fill_value(self, value):
          """
          Check if the value can be inserted into our array without casting,
          and convert it to an appropriate native type if necessary.
  
          Raises
          ------
          TypeError
              If the value cannot be inserted into an array of this dtype.
          """
          if not can_hold_element(self._values, value):
              raise TypeError
          return value
  
      @final
      def _require_scalar(self, value):
          """
          Check that this is a scalar value that we can use for setitem-like
          operations without changing dtype.
          """
          if not is_scalar(value):
              raise TypeError(f"'value' must be a scalar, passed: {type(value).__name__}")
          return value
  
      def _is_memory_usage_qualified(self) -> bool:
          """
          Return a boolean if we need a qualified .info display.
          """
          return self.is_object()
  
      def is_type_compatible(self, kind: str_t) -> bool:
          """
          Whether the index type is compatible with the provided type.
          """
          warnings.warn(
              "Index.is_type_compatible is deprecated and will be removed in a "
              "future version.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          return kind == self.inferred_type
  
      def __contains__(self, key: Any) -> bool:
          """
          Return a boolean indicating whether the provided key is in the index.
  
          Parameters
          ----------
          key : label
              The key to check if it is present in the index.
  
          Returns
          -------
          bool
              Whether the key search is in the index.
  
          Raises
          ------
          TypeError
              If the key is not hashable.
  
          See Also
          --------
          Index.isin : Returns an ndarray of boolean dtype indicating whether the
              list-like key is in the index.
  
          Examples
          --------
          >>> idx = pd.Index([1, 2, 3, 4])
          >>> idx
          Int64Index([1, 2, 3, 4], dtype='int64')
  
          >>> 2 in idx
          True
          >>> 6 in idx
          False
          """
          hash(key)
          try:
              return key in self._engine
          except (OverflowError, TypeError, ValueError):
              return False
  
      # https://github.com/python/typeshed/issues/2148#issuecomment-520783318
      # Incompatible types in assignment (expression has type "None", base class
      # "object" defined the type as "Callable[[object], int]")
      __hash__: None  # type: ignore[assignment]
  
      @final
      def __setitem__(self, key, value):
          raise TypeError("Index does not support mutable operations")
  
      def __getitem__(self, key):
          """
          Override numpy.ndarray's __getitem__ method to work as desired.
  
          This function adds lists and Series as valid boolean indexers
          (ndarrays only supports ndarray with dtype=bool).
  
          If resulting ndim != 1, plain ndarray is returned instead of
          corresponding `Index` subclass.
  
          """
          getitem = self._data.__getitem__
  
          if is_integer(key) or is_float(key):
              # GH#44051 exclude bool, which would return a 2d ndarray
              key = com.cast_scalar_indexer(key, warn_float=True)
              return getitem(key)
  
          if isinstance(key, slice):
              # This case is separated from the conditional above to avoid
              # pessimization com.is_bool_indexer and ndim checks.
              result = getitem(key)
              # Going through simple_new for performance.
              return type(self)._simple_new(result, name=self._name)
  
          if com.is_bool_indexer(key):
              # if we have list[bools, length=1e5] then doing this check+convert
              #  takes 166 µs + 2.1 ms and cuts the ndarray.__getitem__
              #  time below from 3.8 ms to 496 µs
              # if we already have ndarray[bool], the overhead is 1.4 µs or .25%
              key = np.asarray(key, dtype=bool)
  
          result = getitem(key)
          # Because we ruled out integer above, we always get an arraylike here
          if result.ndim > 1:
              deprecate_ndim_indexing(result)
              if hasattr(result, "_ndarray"):
                  # i.e. NDArrayBackedExtensionArray
                  # Unpack to ndarray for MPL compat
                  return result._ndarray
              return result
  
          # NB: Using _constructor._simple_new would break if MultiIndex
          #  didn't override __getitem__
          return self._constructor._simple_new(result, name=self._name)
  
      def _getitem_slice(self: _IndexT, slobj: slice) -> _IndexT:
          """
          Fastpath for __getitem__ when we know we have a slice.
          """
          res = self._data[slobj]
          return type(self)._simple_new(res, name=self._name)
  
      @final
      def _can_hold_identifiers_and_holds_name(self, name) -> bool:
          """
          Faster check for ``name in self`` when we know `name` is a Python
          identifier (e.g. in NDFrame.__getattr__, which hits this to support
          . key lookup). For indexes that can't hold identifiers (everything
          but object & categorical) we just return False.
  
          https://github.com/pandas-dev/pandas/issues/19764
          """
          if self.is_object() or self.is_categorical():
              return name in self
          return False
  
      def append(self, other: Index | Sequence[Index]) -> Index:
          """
          Append a collection of Index options together.
  
          Parameters
          ----------
          other : Index or list/tuple of indices
  
          Returns
          -------
          Index
          """
          to_concat = [self]
  
          if isinstance(other, (list, tuple)):
              to_concat += list(other)
          else:
              # error: Argument 1 to "append" of "list" has incompatible type
              # "Union[Index, Sequence[Index]]"; expected "Index"
              to_concat.append(other)  # type: ignore[arg-type]
  
          for obj in to_concat:
              if not isinstance(obj, Index):
                  raise TypeError("all inputs must be Index")
  
          names = {obj.name for obj in to_concat}
          name = None if len(names) > 1 else self.name
  
          return self._concat(to_concat, name)
  
      def _concat(self, to_concat: list[Index], name: Hashable) -> Index:
          """
          Concatenate multiple Index objects.
          """
          to_concat_vals = [x._values for x in to_concat]
  
          result = concat_compat(to_concat_vals)
  
          is_numeric = result.dtype.kind in ["i", "u", "f"]
          if self._is_backward_compat_public_numeric_index and is_numeric:
              return type(self)._simple_new(result, name=name)
  
          return Index._with_infer(result, name=name)
  
      @final
      def putmask(self, mask, value) -> Index:
          """
          Return a new Index of the values set with the mask.
  
          Returns
          -------
          Index
  
          See Also
          --------
          numpy.ndarray.putmask : Changes elements of an array
              based on conditional and input values.
          """
          mask, noop = validate_putmask(self._values, mask)
          if noop:
              return self.copy()
  
          if value is None and (self._is_numeric_dtype or self.dtype == object):
              value = self._na_value
          try:
              converted = self._validate_fill_value(value)
          except (ValueError, TypeError) as err:
              if is_object_dtype(self):  # pragma: no cover
                  raise err
  
              dtype = self._find_common_type_compat(value)
              return self.astype(dtype).putmask(mask, value)
  
          values = self._values.copy()
  
          if isinstance(values, np.ndarray):
              converted = setitem_datetimelike_compat(values, mask.sum(), converted)
              np.putmask(values, mask, converted)
  
          else:
              # Note: we use the original value here, not converted, as
              #  _validate_fill_value is not idempotent
              values._putmask(mask, value)
  
          return self._shallow_copy(values)
  
      def equals(self, other: Any) -> bool:
          """
          Determine if two Index object are equal.
  
          The things that are being compared are:
  
          * The elements inside the Index object.
          * The order of the elements inside the Index object.
  
          Parameters
          ----------
          other : Any
              The other object to compare against.
  
          Returns
          -------
          bool
              True if "other" is an Index and it has the same elements and order
              as the calling index; False otherwise.
  
          Examples
          --------
          >>> idx1 = pd.Index([1, 2, 3])
          >>> idx1
          Int64Index([1, 2, 3], dtype='int64')
          >>> idx1.equals(pd.Index([1, 2, 3]))
          True
  
          The elements inside are compared
  
          >>> idx2 = pd.Index(["1", "2", "3"])
          >>> idx2
          Index(['1', '2', '3'], dtype='object')
  
          >>> idx1.equals(idx2)
          False
  
          The order is compared
  
          >>> ascending_idx = pd.Index([1, 2, 3])
          >>> ascending_idx
          Int64Index([1, 2, 3], dtype='int64')
          >>> descending_idx = pd.Index([3, 2, 1])
          >>> descending_idx
          Int64Index([3, 2, 1], dtype='int64')
          >>> ascending_idx.equals(descending_idx)
          False
  
          The dtype is *not* compared
  
          >>> int64_idx = pd.Int64Index([1, 2, 3])
          >>> int64_idx
          Int64Index([1, 2, 3], dtype='int64')
          >>> uint64_idx = pd.UInt64Index([1, 2, 3])
          >>> uint64_idx
          UInt64Index([1, 2, 3], dtype='uint64')
          >>> int64_idx.equals(uint64_idx)
          True
          """
          if self.is_(other):
              return True
  
          if not isinstance(other, Index):
              return False
  
          if is_object_dtype(self.dtype) and not is_object_dtype(other.dtype):
              # if other is not object, use other's logic for coercion
              return other.equals(self)
  
          if isinstance(other, ABCMultiIndex):
              # d-level MultiIndex can equal d-tuple Index
              return other.equals(self)
  
          if isinstance(self._values, ExtensionArray):
              # Dispatch to the ExtensionArray's .equals method.
              if not isinstance(other, type(self)):
                  return False
  
              earr = cast(ExtensionArray, self._data)
              return earr.equals(other._data)
  
          if is_extension_array_dtype(other.dtype):
              # All EA-backed Index subclasses override equals
              return other.equals(self)
  
          return array_equivalent(self._values, other._values)
  
      @final
      def identical(self, other) -> bool:
          """
          Similar to equals, but checks that object attributes and types are also equal.
  
          Returns
          -------
          bool
              If two Index objects have equal elements and same type True,
              otherwise False.
          """
          return (
              self.equals(other)
              and all(
                  getattr(self, c, None) == getattr(other, c, None)
                  for c in self._comparables
              )
              and type(self) == type(other)
          )
  
      @final
      def asof(self, label):
          """
          Return the label from the index, or, if not present, the previous one.
  
          Assuming that the index is sorted, return the passed index label if it
          is in the index, or return the previous index label if the passed one
          is not in the index.
  
          Parameters
          ----------
          label : object
              The label up to which the method returns the latest index label.
  
          Returns
          -------
          object
              The passed label if it is in the index. The previous label if the
              passed label is not in the sorted index or `NaN` if there is no
              such label.
  
          See Also
          --------
          Series.asof : Return the latest value in a Series up to the
              passed index.
          merge_asof : Perform an asof merge (similar to left join but it
              matches on nearest key rather than equal key).
          Index.get_loc : An `asof` is a thin wrapper around `get_loc`
              with method='pad'.
  
          Examples
          --------
          `Index.asof` returns the latest index label up to the passed label.
  
          >>> idx = pd.Index(['2013-12-31', '2014-01-02', '2014-01-03'])
          >>> idx.asof('2014-01-01')
          '2013-12-31'
  
          If the label is in the index, the method returns the passed label.
  
          >>> idx.asof('2014-01-02')
          '2014-01-02'
  
          If all of the labels in the index are later than the passed label,
          NaN is returned.
  
          >>> idx.asof('1999-01-02')
          nan
  
          If the index is not sorted, an error is raised.
  
          >>> idx_not_sorted = pd.Index(['2013-12-31', '2015-01-02',
          ...                            '2014-01-03'])
          >>> idx_not_sorted.asof('2013-12-31')
          Traceback (most recent call last):
          ValueError: index must be monotonic increasing or decreasing
          """
          self._searchsorted_monotonic(label)  # validate sortedness
          try:
              loc = self.get_loc(label)
          except (KeyError, TypeError):
              # KeyError -> No exact match, try for padded
              # TypeError -> passed e.g. non-hashable, fall through to get
              #  the tested exception message
              indexer = self.get_indexer([label], method="pad")
              if indexer.ndim > 1 or indexer.size > 1:
                  raise TypeError("asof requires scalar valued input")
              loc = indexer.item()
              if loc == -1:
                  return self._na_value
          else:
              if isinstance(loc, slice):
                  loc = loc.indices(len(self))[-1]
  
          return self[loc]
  
      def asof_locs(self, where: Index, mask: np.ndarray) -> npt.NDArray[np.intp]:
          """
          Return the locations (indices) of labels in the index.
  
          As in the `asof` function, if the label (a particular entry in
          `where`) is not in the index, the latest index label up to the
          passed label is chosen and its index returned.
  
          If all of the labels in the index are later than a label in `where`,
          -1 is returned.
  
          `mask` is used to ignore NA values in the index during calculation.
  
          Parameters
          ----------
          where : Index
              An Index consisting of an array of timestamps.
          mask : np.ndarray[bool]
              Array of booleans denoting where values in the original
              data are not NA.
  
          Returns
          -------
          np.ndarray[np.intp]
              An array of locations (indices) of the labels from the Index
              which correspond to the return values of the `asof` function
              for every element in `where`.
          """
          # error: No overload variant of "searchsorted" of "ndarray" matches argument
          # types "Union[ExtensionArray, ndarray[Any, Any]]", "str"
          # TODO: will be fixed when ExtensionArray.searchsorted() is fixed
          locs = self._values[mask].searchsorted(
              where._values, side="right"  # type: ignore[call-overload]
          )
          locs = np.where(locs > 0, locs - 1, 0)
  
          result = np.arange(len(self), dtype=np.intp)[mask].take(locs)
  
          first_value = self._values[mask.argmax()]
          result[(locs == 0) & (where._values < first_value)] = -1
  
          return result
  
      def sort_values(
          self,
          return_indexer: bool = False,
          ascending: bool = True,
          na_position: str_t = "last",
          key: Callable | None = None,
      ):
          """
          Return a sorted copy of the index.
  
          Return a sorted copy of the index, and optionally return the indices
          that sorted the index itself.
  
          Parameters
          ----------
          return_indexer : bool, default False
              Should the indices that would sort the index be returned.
          ascending : bool, default True
              Should the index values be sorted in an ascending order.
          na_position : {'first' or 'last'}, default 'last'
              Argument 'first' puts NaNs at the beginning, 'last' puts NaNs at
              the end.
  
              .. versionadded:: 1.2.0
  
          key : callable, optional
              If not None, apply the key function to the index values
              before sorting. This is similar to the `key` argument in the
              builtin :meth:`sorted` function, with the notable difference that
              this `key` function should be *vectorized*. It should expect an
              ``Index`` and return an ``Index`` of the same shape.
  
              .. versionadded:: 1.1.0
  
          Returns
          -------
          sorted_index : pandas.Index
              Sorted copy of the index.
          indexer : numpy.ndarray, optional
              The indices that the index itself was sorted by.
  
          See Also
          --------
          Series.sort_values : Sort values of a Series.
          DataFrame.sort_values : Sort values in a DataFrame.
  
          Examples
          --------
          >>> idx = pd.Index([10, 100, 1, 1000])
          >>> idx
          Int64Index([10, 100, 1, 1000], dtype='int64')
  
          Sort values in ascending order (default behavior).
  
          >>> idx.sort_values()
          Int64Index([1, 10, 100, 1000], dtype='int64')
  
          Sort values in descending order, and also get the indices `idx` was
          sorted by.
  
          >>> idx.sort_values(ascending=False, return_indexer=True)
          (Int64Index([1000, 100, 10, 1], dtype='int64'), array([3, 1, 0, 2]))
          """
          idx = ensure_key_mapped(self, key)
  
          # GH 35584. Sort missing values according to na_position kwarg
          # ignore na_position for MultiIndex
          if not isinstance(self, ABCMultiIndex):
              _as = nargsort(
                  items=idx, ascending=ascending, na_position=na_position, key=key
              )
          else:
              _as = idx.argsort()
              if not ascending:
                  _as = _as[::-1]
  
          sorted_index = self.take(_as)
  
          if return_indexer:
              return sorted_index, _as
          else:
              return sorted_index
  
      @final
      def sort(self, *args, **kwargs):
          """
          Use sort_values instead.
          """
          raise TypeError("cannot sort an Index object in-place, use sort_values instead")
  
      def shift(self, periods=1, freq=None):
          """
          Shift index by desired number of time frequency increments.
  
          This method is for shifting the values of datetime-like indexes
          by a specified time increment a given number of times.
  
          Parameters
          ----------
          periods : int, default 1
              Number of periods (or increments) to shift by,
              can be positive or negative.
          freq : pandas.DateOffset, pandas.Timedelta or str, optional
              Frequency increment to shift by.
              If None, the index is shifted by its own `freq` attribute.
              Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc.
  
          Returns
          -------
          pandas.Index
              Shifted index.
  
          See Also
          --------
          Series.shift : Shift values of Series.
  
          Notes
          -----
          This method is only implemented for datetime-like index classes,
          i.e., DatetimeIndex, PeriodIndex and TimedeltaIndex.
  
          Examples
          --------
          Put the first 5 month starts of 2011 into an index.
  
          >>> month_starts = pd.date_range('1/1/2011', periods=5, freq='MS')
          >>> month_starts
          DatetimeIndex(['2011-01-01', '2011-02-01', '2011-03-01', '2011-04-01',
                         '2011-05-01'],
                        dtype='datetime64[ns]', freq='MS')
  
          Shift the index by 10 days.
  
          >>> month_starts.shift(10, freq='D')
          DatetimeIndex(['2011-01-11', '2011-02-11', '2011-03-11', '2011-04-11',
                         '2011-05-11'],
                        dtype='datetime64[ns]', freq=None)
  
          The default value of `freq` is the `freq` attribute of the index,
          which is 'MS' (month start) in this example.
  
          >>> month_starts.shift(10)
          DatetimeIndex(['2011-11-01', '2011-12-01', '2012-01-01', '2012-02-01',
                         '2012-03-01'],
                        dtype='datetime64[ns]', freq='MS')
          """
          raise NotImplementedError(
              f"This method is only implemented for DatetimeIndex, PeriodIndex and "
              f"TimedeltaIndex; Got type {type(self).__name__}"
          )
  
      def argsort(self, *args, **kwargs) -> npt.NDArray[np.intp]:
          """
          Return the integer indices that would sort the index.
  
          Parameters
          ----------
          *args
              Passed to `numpy.ndarray.argsort`.
          **kwargs
              Passed to `numpy.ndarray.argsort`.
  
          Returns
          -------
          np.ndarray[np.intp]
              Integer indices that would sort the index if used as
              an indexer.
  
          See Also
          --------
          numpy.argsort : Similar method for NumPy arrays.
          Index.sort_values : Return sorted copy of Index.
  
          Examples
          --------
          >>> idx = pd.Index(['b', 'a', 'd', 'c'])
          >>> idx
          Index(['b', 'a', 'd', 'c'], dtype='object')
  
          >>> order = idx.argsort()
          >>> order
          array([1, 0, 3, 2])
  
          >>> idx[order]
          Index(['a', 'b', 'c', 'd'], dtype='object')
          """
          # This works for either ndarray or EA, is overridden
          #  by RangeIndex, MultIIndex
          return self._data.argsort(*args, **kwargs)
  
      @final
      def get_value(self, series: Series, key):
          """
          Fast lookup of value from 1-dimensional ndarray.
  
          Only use this if you know what you're doing.
  
          Returns
          -------
          scalar or Series
          """
          warnings.warn(
              "get_value is deprecated and will be removed in a future version. "
              "Use Series[key] instead.",
              FutureWarning,
              stacklevel=find_stack_level(),
          )
  
          self._check_indexing_error(key)
  
          try:
              # GH 20882, 21257
              # First try to convert the key to a location
              # If that fails, raise a KeyError if an integer
              # index, otherwise, see if key is an integer, and
              # try that
              loc = self.get_loc(key)
          except KeyError:
              if not self._should_fallback_to_positional:
                  raise
              elif is_integer(key):
                  # If the Index cannot hold integer, then this is unambiguously
                  #  a locational lookup.
                  loc = key
              else:
                  raise
  
          return self._get_values_for_loc(series, loc, key)
  
      def _check_indexing_error(self, key):
          if not is_scalar(key):
              # if key is not a scalar, directly raise an error (the code below
              # would convert to numpy arrays and raise later any way) - GH29926
              raise InvalidIndexError(key)
  
      @cache_readonly
      def _should_fallback_to_positional(self) -> bool:
          """
          Should an integer key be treated as positional?
          """
          return not self.holds_integer() and not self.is_boolean()
  
      def _get_values_for_loc(self, series: Series, loc, key):
          """
          Do a positional lookup on the given Series, returning either a scalar
          or a Series.
  
          Assumes that `series.index is self`
  
          key is included for MultiIndex compat.
          """
          if is_integer(loc):
              return series._values[loc]
  
          return series.iloc[loc]
  
      @final
      def set_value(self, arr, key, value):
          """
          Fast lookup of value from 1-dimensional ndarray.
  
          .. deprecated:: 1.0
  
          Notes
          -----
          Only use this if you know what you're doing.
          """
          warnings.warn(
              (
                  "The 'set_value' method is deprecated, and "
                  "will be removed in a future version."
              ),
              FutureWarning,
              stacklevel=find_stack_level(),
          )
          loc = self._engine.get_loc(key)
          validate_numeric_casting(arr.dtype, value)
          arr[loc] = value
  
      _index_shared_docs[
          "get_indexer_non_unique"
      ] = """
          Compute indexer and mask for new index given the current index. The
          indexer should be then used as an input to ndarray.take to align the
          current data to the new index.
  
          Parameters
          ----------
          target : %(target_klass)s
  
          Returns
          -------
          indexer : np.ndarray[np.intp]
              Integers from 0 to n - 1 indicating that the index at these
              positions matches the corresponding target values. Missing values
              in the target are marked by -1.
          missing : np.ndarray[np.intp]
              An indexer into the target of the values not found.
              These correspond to the -1 in the indexer array.
          """
  
      @Appender(_index_shared_docs["get_indexer_non_unique"] % _index_doc_kwargs)
      def get_indexer_non_unique(
          self, target
      ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          target = ensure_index(target)
          target = self._maybe_cast_listlike_indexer(target)
  
          if not self._should_compare(target) and not is_interval_dtype(self.dtype):
              # IntervalIndex get special treatment bc numeric scalars can be
              #  matched to Interval scalars
              return self._get_indexer_non_comparable(target, method=None, unique=False)
  
          pself, ptarget = self._maybe_promote(target)
          if pself is not self or ptarget is not target:
              return pself.get_indexer_non_unique(ptarget)
  
          if not is_dtype_equal(self.dtype, target.dtype):
              # TODO: if object, could use infer_dtype to preempt costly
              #  conversion if still non-comparable?
              dtype = self._find_common_type_compat(target)
  
              this = self.astype(dtype, copy=False)
              that = target.astype(dtype, copy=False)
              return this.get_indexer_non_unique(that)
  
          # Note: _maybe_promote ensures we never get here with MultiIndex
          #  self and non-Multi target
          tgt_values = target._get_engine_target()
          if self._is_multi and target._is_multi:
              engine = self._engine
              # error: "IndexEngine" has no attribute "_extract_level_codes"
              tgt_values = engine._extract_level_codes(  # type: ignore[attr-defined]
                  target
              )
  
          indexer, missing = self._engine.get_indexer_non_unique(tgt_values)
          return ensure_platform_int(indexer), ensure_platform_int(missing)
  
      @final
      def get_indexer_for(self, target) -> npt.NDArray[np.intp]:
          """
          Guaranteed return of an indexer even when non-unique.
  
          This dispatches to get_indexer or get_indexer_non_unique
          as appropriate.
  
          Returns
          -------
          np.ndarray[np.intp]
              List of indices.
  
          Examples
          --------
          >>> idx = pd.Index([np.nan, 'var1', np.nan])
          >>> idx.get_indexer_for([np.nan])
          array([0, 2])
          """
          if self._index_as_unique:
              return self.get_indexer(target)
          indexer, _ = self.get_indexer_non_unique(target)
          return indexer
  
      def _get_indexer_strict(self, key, axis_name: str_t) -> tuple[Index, np.ndarray]:
          """
          Analogue to get_indexer that raises if any elements are missing.
          """
          keyarr = key
          if not isinstance(keyarr, Index):
              keyarr = com.asarray_tuplesafe(keyarr)
  
          if self._index_as_unique:
              indexer = self.get_indexer_for(keyarr)
              keyarr = self.reindex(keyarr)[0]
          else:
              keyarr, indexer, new_indexer = self._reindex_non_unique(keyarr)
  
          self._raise_if_missing(keyarr, indexer, axis_name)
  
          keyarr = self.take(indexer)
          if isinstance(key, Index):
              # GH 42790 - Preserve name from an Index
              keyarr.name = key.name
          if keyarr.dtype.kind in ["m", "M"]:
              # DTI/TDI.take can infer a freq in some cases when we dont want one
              if isinstance(key, list) or (
                  isinstance(key, type(self))
                  # "Index" has no attribute "freq"
                  and key.freq is None  # type: ignore[attr-defined]
              ):
                  keyarr = keyarr._with_freq(None)
  
          return keyarr, indexer
  
      def _raise_if_missing(self, key, indexer, axis_name: str_t) -> None:
          """
          Check that indexer can be used to return a result.
  
          e.g. at least one element was found,
          unless the list of keys was actually empty.
  
          Parameters
          ----------
          key : list-like
              Targeted labels (only used to show correct error message).
          indexer: array-like of booleans
              Indices corresponding to the key,
              (with -1 indicating not found).
          axis_name : str
  
          Raises
          ------
          KeyError
              If at least one key was requested but none was found.
          """
          if len(key) == 0:
              return
  
          # Count missing values
          missing_mask = indexer < 0
          nmissing = missing_mask.sum()
  
          if nmissing:
  
              # TODO: remove special-case; this is just to keep exception
              #  message tests from raising while debugging
              use_interval_msg = is_interval_dtype(self.dtype) or (
                  is_categorical_dtype(self.dtype)
                  # "Index" has no attribute "categories"  [attr-defined]
                  and is_interval_dtype(
                      self.categories.dtype  # type: ignore[attr-defined]
                  )
              )
  
              if nmissing == len(indexer):
                  if use_interval_msg:
                      key = list(key)
                  raise KeyError(f"None of [{key}] are in the [{axis_name}]")
  
              not_found = list(ensure_index(key)[missing_mask.nonzero()[0]].unique())
              raise KeyError(f"{not_found} not in index")
  
      @overload
      def _get_indexer_non_comparable(
          self, target: Index, method, unique: Literal[True] = ...
      ) -> npt.NDArray[np.intp]:
          ...
  
      @overload
      def _get_indexer_non_comparable(
          self, target: Index, method, unique: Literal[False]
      ) -> tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          ...
  
      @overload
      def _get_indexer_non_comparable(
          self, target: Index, method, unique: bool = True
      ) -> npt.NDArray[np.intp] | tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          ...
  
      @final
      def _get_indexer_non_comparable(
          self, target: Index, method, unique: bool = True
      ) -> npt.NDArray[np.intp] | tuple[npt.NDArray[np.intp], npt.NDArray[np.intp]]:
          """
          Called from get_indexer or get_indexer_non_unique when the target
          is of a non-comparable dtype.
  
          For get_indexer lookups with method=None, get_indexer is an _equality_
          check, so non-comparable dtypes mean we will always have no matches.
  
          For get_indexer lookups with a method, get_indexer is an _inequality_
          check, so non-comparable dtypes mean we will always raise TypeError.
  
          Parameters
          ----------
          target : Index
          method : str or None
          unique : bool, default True
              * True if called from get_indexer.
              * False if called from get_indexer_non_unique.
  
          Raises
          ------
          TypeError
              If doing an inequality check, i.e. method is not None.
          """
          if method is not None:
              other = unpack_nested_dtype(target)
              raise TypeError(f"Cannot compare dtypes {self.dtype} and {other.dtype}")
  
          no_matches = -1 * np.ones(target.shape, dtype=np.intp)
          if unique:
              # This is for get_indexer
              return no_matches
          else:
              # This is for get_indexer_non_unique
              missing = np.arange(len(target), dtype=np.intp)
              return no_matches, missing
  
      @property
      def _index_as_unique(self) -> bool:
          """
          Whether we should treat this as unique for the sake of
          get_indexer vs get_indexer_non_unique.
  
          For IntervalIndex compat.
          """
          return self.is_unique
  
      _requires_unique_msg = "Reindexing only valid with uniquely valued Index objects"
  
      @final
      def _maybe_promote(self, other: Index) -> tuple[Index, Index]:
          """
          When dealing with an object-dtype Index and a non-object Index, see
          if we can upcast the object-dtype one to improve performance.
          """
  
          if isinstance(self, ABCDatetimeIndex) and isinstance(other, ABCDatetimeIndex):
              if (
                  self.tz is not None
                  and other.tz is not None
                  and not tz_compare(self.tz, other.tz)
              ):
                  # standardize on UTC
                  return self.tz_convert("UTC"), other.tz_convert("UTC")
  
          elif self.inferred_type == "date" and isinstance(other, ABCDatetimeIndex):
              try:
                  return type(other)(self), other
              except OutOfBoundsDatetime:
                  return self, other
          elif self.inferred_type == "timedelta" and isinstance(other, ABCTimedeltaIndex):
              # TODO: we dont have tests that get here
              return type(other)(self), other
          elif self.inferred_type == "boolean":
              if not is_object_dtype(self.dtype):
                  return self.astype("object"), other.astype("object")
  
          elif self.dtype.kind == "u" and other.dtype.kind == "i":
              # GH#41873
              if other.min() >= 0:
                  # lookup min as it may be cached
                  # TODO: may need itemsize check if we have non-64-bit Indexes
                  return self, other.astype(self.dtype)
  
          elif self._is_multi and not other._is_multi:
              try:
                  # "Type[Index]" has no attribute "from_tuples"
                  other = type(self).from_tuples(other)  # type: ignore[attr-defined]
              except (TypeError, ValueError):
                  # let's instead try with a straight Index
                  self = Index(self._values)
  
          if not is_object_dtype(self.dtype) and is_object_dtype(other.dtype):
              # Reverse op so we dont need to re-implement on the subclasses
              other, self = other._maybe_promote(self)
  
          return self, other
  
      @final
      def _find_common_type_compat(self, target) -> DtypeObj:
          """
          Implementation of find_common_type that adjusts for Index-specific
          special cases.
          """
          if is_interval_dtype(self.dtype) and is_valid_na_for_dtype(target, self.dtype):
              # e.g. setting NA value into IntervalArray[int64]
              self = cast("IntervalIndex", self)
              return IntervalDtype(np.float64, closed=self.closed)
  
          target_dtype, _ = infer_dtype_from(target, pandas_dtype=True)
  
          # special case: if one dtype is uint64 and the other a signed int, return object
          # See https://github.com/pandas-dev/pandas/issues/26778 for discussion
          # Now it's:
          # * float | [u]int -> float
          # * uint64 | signed int  -> object
          # We may change union(float | [u]int) to go to object.
          if self.dtype == "uint64" or target_dtype == "uint64":
              if is_signed_integer_dtype(self.dtype) or is_signed_integer_dtype(
                  target_dtype
              ):
                  return np.dtype("object")
  
          dtype = find_common_type([self.dtype, target_dtype])
  
          if dtype.kind in ["i", "u"]:
              # TODO: what about reversed with self being categorical?
              if (
                  isinstance(target, Index)
                  and is_categorical_dtype(target.dtype)
                  and target.hasnans
              ):
                  # FIXME: find_common_type incorrect with Categorical GH#38240
                  # FIXME: some cases where float64 cast can be lossy?
                  dtype = np.dtype(np.float64)
          if dtype.kind == "c":
              dtype = np.dtype(object)
          return dtype
  
      @final
      def _should_compare(self, other: Index) -> bool:
          """
          Check if `self == other` can ever have non-False entries.
          """
  
          if (other.is_boolean() and self.is_numeric()) or (
              self.is_boolean() and other.is_numeric()
          ):
              # GH#16877 Treat boolean labels passed to a numeric index as not
              #  found. Without this fix False and True would be treated as 0 and 1
              #  respectively.
              return False
  
          other = unpack_nested_dtype(other)
          dtype = other.dtype
          return self._is_comparable_dtype(dtype) or is_object_dtype(dtype)
  
      def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
          """
          Can we compare values of the given dtype to our own?
          """
          return True
  
      @final
      def groupby(self, values) -> PrettyDict[Hashable, np.ndarray]:
          """
          Group the index labels by a given array of values.
  
          Parameters
          ----------
          values : array
              Values used to determine the groups.
  
          Returns
          -------
          dict
              {group name -> group labels}
          """
          # TODO: if we are a MultiIndex, we can do better
          # that converting to tuples
          if isinstance(values, ABCMultiIndex):
              values = values._values
          values = Categorical(values)
          result = values._reverse_indexer()
  
          # map to the label
          result = {k: self.take(v) for k, v in result.items()}
  
          return PrettyDict(result)
  
      def map(self, mapper, na_action=None):
          """
          Map values using input correspondence (a dict, Series, or function).
  
          Parameters
          ----------
          mapper : function, dict, or Series
              Mapping correspondence.
          na_action : {None, 'ignore'}
              If 'ignore', propagate NA values, without passing them to the
              mapping correspondence.
  
          Returns
          -------
          applied : Union[Index, MultiIndex], inferred
              The output of the mapping function applied to the index.
              If the function returns a tuple with more than one element
              a MultiIndex will be returned.
          """
          from pandas.core.indexes.multi import MultiIndex
  
          new_values = self._map_values(mapper, na_action=na_action)
  
          # we can return a MultiIndex
          if new_values.size and isinstance(new_values[0], tuple):
              if isinstance(self, MultiIndex):
                  names = self.names
              elif self.name:
                  names = [self.name] * len(new_values[0])
              else:
                  names = None
              return MultiIndex.from_tuples(new_values, names=names)
  
          dtype = None
          if not new_values.size:
              # empty
              dtype = self.dtype
  
          # e.g. if we are floating and new_values is all ints, then we
          #  don't want to cast back to floating.  But if we are UInt64
          #  and new_values is all ints, we want to try.
          same_dtype = lib.infer_dtype(new_values, skipna=False) == self.inferred_type
          if same_dtype:
              new_values = maybe_cast_pointwise_result(
                  new_values, self.dtype, same_dtype=same_dtype
              )
  
          if self._is_backward_compat_public_numeric_index and is_numeric_dtype(
              new_values.dtype
          ):
              return self._constructor(
                  new_values, dtype=dtype, copy=False, name=self.name
              )
  
          return Index._with_infer(new_values, dtype=dtype, copy=False, name=self.name)
  
      # TODO: De-duplicate with map, xref GH#32349
      @final
      def _transform_index(self, func, *, level=None) -> Index:
          """
          Apply function to all values found in index.
  
          This includes transforming multiindex entries separately.
          Only apply function to one level of the MultiIndex if level is specified.
          """
          if isinstance(self, ABCMultiIndex):
              if level is not None:
                  # Caller is responsible for ensuring level is positional.
                  items = [
                      tuple(func(y) if i == level else y for i, y in enumerate(x))
                      for x in self
                  ]
              else:
                  items = [tuple(func(y) for y in x) for x in self]
              return type(self).from_tuples(items, names=self.names)
          else:
              items = [func(x) for x in self]
              return Index(items, name=self.name, tupleize_cols=False)
  
      def isin(self, values, level=None) -> np.ndarray:
          """
          Return a boolean array where the index values are in `values`.
  
          Compute boolean array of whether each index value is found in the
          passed set of values. The length of the returned boolean array matches
          the length of the index.
  
          Parameters
          ----------
          values : set or list-like
              Sought values.
          level : str or int, optional
              Name or position of the index level to use (if the index is a
              `MultiIndex`).
  
          Returns
          -------
          np.ndarray[bool]
              NumPy array of boolean values.
  
          See Also
          --------
          Series.isin : Same for Series.
          DataFrame.isin : Same method for DataFrames.
  
          Notes
          -----
          In the case of `MultiIndex` you must either specify `values` as a
          list-like object containing tuples that are the same length as the
          number of levels, or specify `level`. Otherwise it will raise a
          ``ValueError``.
  
          If `level` is specified:
  
          - if it is the name of one *and only one* index level, use that level;
          - otherwise it should be a number indicating level position.
  
          Examples
          --------
          >>> idx = pd.Index([1,2,3])
          >>> idx
          Int64Index([1, 2, 3], dtype='int64')
  
          Check whether each index value in a list of values.
  
          >>> idx.isin([1, 4])
          array([ True, False, False])
  
          >>> midx = pd.MultiIndex.from_arrays([[1,2,3],
          ...                                  ['red', 'blue', 'green']],
          ...                                  names=('number', 'color'))
          >>> midx
          MultiIndex([(1,   'red'),
                      (2,  'blue'),
                      (3, 'green')],
                     names=['number', 'color'])
  
          Check whether the strings in the 'color' level of the MultiIndex
          are in a list of colors.
  
          >>> midx.isin(['red', 'orange', 'yellow'], level='color')
          array([ True, False, False])
  
          To check across the levels of a MultiIndex, pass a list of tuples:
  
          >>> midx.isin([(1, 'red'), (3, 'red')])
          array([ True, False, False])
  
          For a DatetimeIndex, string values in `values` are converted to
          Timestamps.
  
          >>> dates = ['2000-03-11', '2000-03-12', '2000-03-13']
          >>> dti = pd.to_datetime(dates)
          >>> dti
          DatetimeIndex(['2000-03-11', '2000-03-12', '2000-03-13'],
          dtype='datetime64[ns]', freq=None)
  
          >>> dti.isin(['2000-03-11'])
          array([ True, False, False])
          """
          if level is not None:
              self._validate_index_level(level)
          return algos.isin(self._values, values)
  
      def _get_string_slice(self, key: str_t):
          # this is for partial string indexing,
          # overridden in DatetimeIndex, TimedeltaIndex and PeriodIndex
          raise NotImplementedError
  
      def slice_indexer(
          self,
          start: Hashable | None = None,
          end: Hashable | None = None,
          step: int | None = None,
          kind=no_default,
      ) -> slice:
          """
          Compute the slice indexer for input labels and step.
  
          Index needs to be ordered and unique.
  
          Parameters
          ----------
          start : label, default None
              If None, defaults to the beginning.
          end : label, default None
              If None, defaults to the end.
          step : int, default None
          kind : str, default None
  
              .. deprecated:: 1.4.0
  
          Returns
          -------
          indexer : slice
  
          Raises
          ------
          KeyError : If key does not exist, or key is not unique and index is
              not ordered.
  
          Notes
          -----
          This function assumes that the data is sorted, so use at your own peril
  
          Examples
          --------
          This is a method on all index types. For example you can do:
  
          >>> idx = pd.Index(list('abcd'))
          >>> idx.slice_indexer(start='b', end='c')
          slice(1, 3, None)
  
          >>> idx = pd.MultiIndex.from_arrays([list('abcd'), list('efgh')])
          >>> idx.slice_indexer(start='b', end=('c', 'g'))
          slice(1, 3, None)
          """
          self._deprecated_arg(kind, "kind", "slice_indexer")
  
          start_slice, end_slice = self.slice_locs(start, end, step=step)
  
          # return a slice
          if not is_scalar(start_slice):
              raise AssertionError("Start slice bound is non-scalar")
          if not is_scalar(end_slice):
              raise AssertionError("End slice bound is non-scalar")
  
          return slice(start_slice, end_slice, step)
  
      def _maybe_cast_indexer(self, key):
          """
          If we have a float key and are not a floating index, then try to cast
          to an int if equivalent.
          """
          if not self.is_floating():
              return com.cast_scalar_indexer(key)
          return key
  
      def _maybe_cast_listlike_indexer(self, target) -> Index:
          """
          Analogue to maybe_cast_indexer for get_indexer instead of get_loc.
          """
          return ensure_index(target)
  
      @final
      def _validate_indexer(self, form: str_t, key, kind: str_t):
          """
          If we are positional indexer, validate that we have appropriate
          typed bounds must be an integer.
          """
          assert kind in ["getitem", "iloc"]
  
          if key is not None and not is_integer(key):
              raise self._invalid_indexer(form, key)
  
      def _maybe_cast_slice_bound(self, label, side: str_t, kind=no_default):
          """
          This function should be overloaded in subclasses that allow non-trivial
          casting on label-slice bounds, e.g. datetime-like indices allowing
          strings containing formatted datetimes.
  
          Parameters
          ----------
          label : object
          side : {'left', 'right'}
          kind : {'loc', 'getitem'} or None
  
              .. deprecated:: 1.3.0
  
          Returns
          -------
          label : object
  
          Notes
          -----
          Value of `side` parameter should be validated in caller.
          """
          assert kind in ["loc", "getitem", None, no_default]
          self._deprecated_arg(kind, "kind", "_maybe_cast_slice_bound")
  
          # We are a plain index here (sub-class override this method if they
          # wish to have special treatment for floats/ints, e.g. Float64Index and
          # datetimelike Indexes
          # reject them, if index does not contain label
          if (is_float(label) or is_integer(label)) and label not in self:
              raise self._invalid_indexer("slice", label)
  
          return label
  
      def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"):
          if self.is_monotonic_increasing:
              return self.searchsorted(label, side=side)
          elif self.is_monotonic_decreasing:
              # np.searchsorted expects ascending sort order, have to reverse
              # everything for it to work (element ordering, search side and
              # resulting value).
              pos = self[::-1].searchsorted(
                  label, side="right" if side == "left" else "left"
              )
              return len(self) - pos
  
          raise ValueError("index must be monotonic increasing or decreasing")
  
      def get_slice_bound(
          self, label, side: Literal["left", "right"], kind=no_default
      ) -> int:
          """
          Calculate slice bound that corresponds to given label.
  
          Returns leftmost (one-past-the-rightmost if ``side=='right'``) position
          of given label.
  
          Parameters
          ----------
          label : object
          side : {'left', 'right'}
          kind : {'loc', 'getitem'} or None
  
              .. deprecated:: 1.4.0
  
          Returns
          -------
          int
              Index of label.
          """
          assert kind in ["loc", "getitem", None, no_default]
          self._deprecated_arg(kind, "kind", "get_slice_bound")
  
          if side not in ("left", "right"):
              raise ValueError(
                  "Invalid value for side kwarg, must be either "
                  f"'left' or 'right': {side}"
              )
  
          original_label = label
  
          # For datetime indices label may be a string that has to be converted
          # to datetime boundary according to its resolution.
          label = self._maybe_cast_slice_bound(label, side)
  
          # we need to look up the label
          try:
              slc = self.get_loc(label)
          except KeyError as err:
              try:
                  return self._searchsorted_monotonic(label, side)
              except ValueError:
                  # raise the original KeyError
                  raise err
  
          if isinstance(slc, np.ndarray):
              # get_loc may return a boolean array, which
              # is OK as long as they are representable by a slice.
              assert is_bool_dtype(slc.dtype)
              slc = lib.maybe_booleans_to_slice(slc.view("u1"))
              if isinstance(slc, np.ndarray):
                  raise KeyError(
                      f"Cannot get {side} slice bound for non-unique "
                      f"label: {repr(original_label)}"
                  )
  
          if isinstance(slc, slice):
              if side == "left":
                  return slc.start
              else:
                  return slc.stop
          else:
              if side == "right":
                  return slc + 1
              else:
                  return slc
  
      def slice_locs(
          self, start=None, end=None, step=None, kind=no_default
      ) -> tuple[int, int]:
          """
          Compute slice locations for input labels.
  
          Parameters
          ----------
          start : label, default None
              If None, defaults to the beginning.
          end : label, default None
              If None, defaults to the end.
          step : int, defaults None
              If None, defaults to 1.
          kind : {'loc', 'getitem'} or None
  
              .. deprecated:: 1.4.0
  
          Returns
          -------
          start, end : int
  
          See Also
          --------
          Index.get_loc : Get location for a single label.
  
          Notes
          -----
          This method only works if the index is monotonic or unique.
  
          Examples
          --------
          >>> idx = pd.Index(list('abcd'))
          >>> idx.slice_locs(start='b', end='c')
          (1, 3)
          """
          self._deprecated_arg(kind, "kind", "slice_locs")
          inc = step is None or step >= 0
  
          if not inc:
              # If it's a reverse slice, temporarily swap bounds.
              start, end = end, start
  
          # GH 16785: If start and end happen to be date strings with UTC offsets
          # attempt to parse and check that the offsets are the same
          if isinstance(start, (str, datetime)) and isinstance(end, (str, datetime)):
              try:
                  ts_start = Timestamp(start)
                  ts_end = Timestamp(end)
              except (ValueError, TypeError):
                  pass
              else:
                  if not tz_compare(ts_start.tzinfo, ts_end.tzinfo):
                      raise ValueError("Both dates must have the same UTC offset")
  
          start_slice = None
          if start is not None:
              start_slice = self.get_slice_bound(start, "left")
          if start_slice is None:
              start_slice = 0
  
          end_slice = None
          if end is not None:
              end_slice = self.get_slice_bound(end, "right")
          if end_slice is None:
              end_slice = len(self)
  
          if not inc:
              # Bounds at this moment are swapped, swap them back and shift by 1.
              #
              # slice_locs('B', 'A', step=-1): s='B', e='A'
              #
              #              s='A'                 e='B'
              # AFTER SWAP:    |                     |
              #                v ------------------> V
              #           -----------------------------------
              #           | | |A|A|A|A| | | | | |B|B| | | | |
              #           -----------------------------------
              #              ^ <------------------ ^
              # SHOULD BE:   |                     |
              #           end=s-1              start=e-1
              #
              end_slice, start_slice = start_slice - 1, end_slice - 1
  
              # i == -1 triggers ``len(self) + i`` selection that points to the
              # last element, not before-the-first one, subtracting len(self)
              # compensates that.
              if end_slice == -1:
                  end_slice -= len(self)
              if start_slice == -1:
                  start_slice -= len(self)
  
          return start_slice, end_slice
  
      def delete(self: _IndexT, loc) -> _IndexT:
          """
          Make new Index with passed location(-s) deleted.
  
          Parameters
          ----------
          loc : int or list of int
              Location of item(-s) which will be deleted.
              Use a list of locations to delete more than one value at the same time.
  
          Returns
          -------
          Index
              Will be same type as self, except for RangeIndex.
  
          See Also
          --------
          numpy.delete : Delete any rows and column from NumPy array (ndarray).
  
          Examples
          --------
          >>> idx = pd.Index(['a', 'b', 'c'])
          >>> idx.delete(1)
          Index(['a', 'c'], dtype='object')
  
          >>> idx = pd.Index(['a', 'b', 'c'])
          >>> idx.delete([0, 2])
          Index(['b'], dtype='object')
          """
          values = self._values
          if isinstance(values, np.ndarray):
              # TODO(__array_function__): special casing will be unnecessary
              res_values = np.delete(values, loc)
          else:
              res_values = values.delete(loc)
  
          # _constructor so RangeIndex->Int64Index
          return self._constructor._simple_new(res_values, name=self.name)
  
      def insert(self, loc: int, item) -> Index:
          """
          Make new Index inserting new item at location.
  
          Follows Python numpy.insert semantics for negative values.
  
          Parameters
          ----------
          loc : int
          item : object
  
          Returns
          -------
          new_index : Index
          """
          # Note: this method is overridden by all ExtensionIndex subclasses,
          #  so self is never backed by an EA.
          item = lib.item_from_zerodim(item)
          if is_valid_na_for_dtype(item, self.dtype) and self.dtype != object:
              item = self._na_value
  
          arr = self._values
  
          try:
              if isinstance(arr, ExtensionArray):
                  res_values = arr.insert(loc, item)
                  return type(self)._simple_new(res_values, name=self.name)
              else:
                  item = self._validate_fill_value(item)
          except (TypeError, ValueError):
              # e.g. trying to insert an integer into a DatetimeIndex
              #  We cannot keep the same dtype, so cast to the (often object)
              #  minimal shared dtype before doing the insert.
              dtype = self._find_common_type_compat(item)
              return self.astype(dtype).insert(loc, item)
  
          if arr.dtype != object or not isinstance(
              item, (tuple, np.datetime64, np.timedelta64)
          ):
              # with object-dtype we need to worry about numpy incorrectly casting
              # dt64/td64 to integer, also about treating tuples as sequences
              # special-casing dt64/td64 https://github.com/numpy/numpy/issues/12550
              casted = arr.dtype.type(item)
              new_values = np.insert(arr, loc, casted)
  
          else:
              new_values = np.insert(arr, loc, None)
              loc = loc if loc >= 0 else loc - 1
              new_values[loc] = item
  
          # Use self._constructor instead of Index to retain NumericIndex GH#43921
          # TODO(2.0) can use Index instead of self._constructor
          return self._constructor._with_infer(new_values, name=self.name)
  
      def drop(self, labels, errors: str_t = "raise") -> Index:
          """
          Make new Index with passed list of labels deleted.
  
          Parameters
          ----------
          labels : array-like or scalar
          errors : {'ignore', 'raise'}, default 'raise'
              If 'ignore', suppress error and existing labels are dropped.
  
          Returns
          -------
          dropped : Index
              Will be same type as self, except for RangeIndex.
  
          Raises
          ------
          KeyError
              If not all of the labels are found in the selected axis
          """
          if not isinstance(labels, Index):
              # avoid materializing e.g. RangeIndex
              arr_dtype = "object" if self.dtype == "object" else None
              labels = com.index_labels_to_array(labels, dtype=arr_dtype)
  
          indexer = self.get_indexer_for(labels)
          mask = indexer == -1
          if mask.any():
              if errors != "ignore":
                  raise KeyError(f"{list(labels[mask])} not found in axis")
              indexer = indexer[~mask]
          return self.delete(indexer)
  
      # --------------------------------------------------------------------
      # Generated Arithmetic, Comparison, and Unary Methods
  
      def _cmp_method(self, other, op):
          """
          Wrapper used to dispatch comparison operations.
          """
          if self.is_(other):
              # fastpath
              if op in {operator.eq, operator.le, operator.ge}:
                  arr = np.ones(len(self), dtype=bool)
                  if self._can_hold_na and not isinstance(self, ABCMultiIndex):
                      # TODO: should set MultiIndex._can_hold_na = False?
                      arr[self.isna()] = False
                  return arr
              elif op in {operator.ne, operator.lt, operator.gt}:
                  arr = np.zeros(len(self), dtype=bool)
                  if self._can_hold_na and not isinstance(self, ABCMultiIndex):
                      arr[self.isna()] = True
                  return arr
  
          if isinstance(other, (np.ndarray, Index, ABCSeries, ExtensionArray)) and len(
              self
          ) != len(other):
              raise ValueError("Lengths must match to compare")
  
          if not isinstance(other, ABCMultiIndex):
              other = extract_array(other, extract_numpy=True)
          else:
              other = np.asarray(other)
  
          if is_object_dtype(self.dtype) and isinstance(other, ExtensionArray):
              # e.g. PeriodArray, Categorical
              with np.errstate(all="ignore"):
                  result = op(self._values, other)
  
          elif isinstance(self._values, ExtensionArray):
              result = op(self._values, other)
  
          elif is_object_dtype(self.dtype) and not isinstance(self, ABCMultiIndex):
              # don't pass MultiIndex
              with np.errstate(all="ignore"):
                  result = ops.comp_method_OBJECT_ARRAY(op, self._values, other)
  
          else:
              with np.errstate(all="ignore"):
                  result = ops.comparison_op(self._values, other, op)
  
          return result
  
      def _construct_result(self, result, name):
          if isinstance(result, tuple):
              return (
                  Index._with_infer(result[0], name=name),
                  Index._with_infer(result[1], name=name),
              )
          return Index._with_infer(result, name=name)
  
      def _arith_method(self, other, op):
          if (
              isinstance(other, Index)
              and is_object_dtype(other.dtype)
              and type(other) is not Index
          ):
              # We return NotImplemented for object-dtype index *subclasses* so they have
              # a chance to implement ops before we unwrap them.
              # See https://github.com/pandas-dev/pandas/issues/31109
              return NotImplemented
  
          return super()._arith_method(other, op)
  
      @final
      def _unary_method(self, op):
          result = op(self._values)
          return Index(result, name=self.name)
  
      def __abs__(self):
          return self._unary_method(operator.abs)
  
      def __neg__(self):
          return self._unary_method(operator.neg)
  
      def __pos__(self):
          return self._unary_method(operator.pos)
  
      def __invert__(self):
          # GH#8875
          return self._unary_method(operator.inv)
  
      # --------------------------------------------------------------------
      # Reductions
  
      def any(self, *args, **kwargs):
          """
          Return whether any element is Truthy.
  
          Parameters
          ----------
          *args
              Required for compatibility with numpy.
          **kwargs
              Required for compatibility with numpy.
  
          Returns
          -------
          any : bool or array-like (if axis is specified)
              A single element array-like may be converted to bool.
  
          See Also
          --------
          Index.all : Return whether all elements are True.
          Series.all : Return whether all elements are True.
  
          Notes
          -----
          Not a Number (NaN), positive infinity and negative infinity
          evaluate to True because these are not equal to zero.
  
          Examples
          --------
          >>> index = pd.Index([0, 1, 2])
          >>> index.any()
          True
  
          >>> index = pd.Index([0, 0, 0])
          >>> index.any()
          False
          """
          nv.validate_any(args, kwargs)
          self._maybe_disable_logical_methods("any")
          # error: Argument 1 to "any" has incompatible type "ArrayLike"; expected
          # "Union[Union[int, float, complex, str, bytes, generic], Sequence[Union[int,
          # float, complex, str, bytes, generic]], Sequence[Sequence[Any]],
          # _SupportsArray]"
          return np.any(self.values)  # type: ignore[arg-type]
  
      def all(self, *args, **kwargs):
          """
          Return whether all elements are Truthy.
  
          Parameters
          ----------
          *args
              Required for compatibility with numpy.
          **kwargs
              Required for compatibility with numpy.
  
          Returns
          -------
          all : bool or array-like (if axis is specified)
              A single element array-like may be converted to bool.
  
          See Also
          --------
          Index.any : Return whether any element in an Index is True.
          Series.any : Return whether any element in a Series is True.
          Series.all : Return whether all elements in a Series are True.
  
          Notes
          -----
          Not a Number (NaN), positive infinity and negative infinity
          evaluate to True because these are not equal to zero.
  
          Examples
          --------
          True, because nonzero integers are considered True.
  
          >>> pd.Index([1, 2, 3]).all()
          True
  
          False, because ``0`` is considered False.
  
          >>> pd.Index([0, 1, 2]).all()
          False
          """
          nv.validate_all(args, kwargs)
          self._maybe_disable_logical_methods("all")
          # error: Argument 1 to "all" has incompatible type "ArrayLike"; expected
          # "Union[Union[int, float, complex, str, bytes, generic], Sequence[Union[int,
          # float, complex, str, bytes, generic]], Sequence[Sequence[Any]],
          # _SupportsArray]"
          return np.all(self.values)  # type: ignore[arg-type]
  
      @final
      def _maybe_disable_logical_methods(self, opname: str_t) -> None:
          """
          raise if this Index subclass does not support any or all.
          """
          if (
              isinstance(self, ABCMultiIndex)
              or needs_i8_conversion(self.dtype)
              or is_interval_dtype(self.dtype)
              or is_categorical_dtype(self.dtype)
              or is_float_dtype(self.dtype)
          ):
              # This call will raise
              make_invalid_op(opname)(self)
  
      @Appender(IndexOpsMixin.argmin.__doc__)
      def argmin(self, axis=None, skipna=True, *args, **kwargs):
          nv.validate_argmin(args, kwargs)
          nv.validate_minmax_axis(axis)
  
          if not self._is_multi and self.hasnans:
              # Take advantage of cache
              mask = self._isnan
              if not skipna or mask.all():
                  return -1
          return super().argmin(skipna=skipna)
  
      @Appender(IndexOpsMixin.argmax.__doc__)
      def argmax(self, axis=None, skipna=True, *args, **kwargs):
          nv.validate_argmax(args, kwargs)
          nv.validate_minmax_axis(axis)
  
          if not self._is_multi and self.hasnans:
              # Take advantage of cache
              mask = self._isnan
              if not skipna or mask.all():
                  return -1
          return super().argmax(skipna=skipna)
  
      @doc(IndexOpsMixin.min)
      def min(self, axis=None, skipna=True, *args, **kwargs):
          nv.validate_min(args, kwargs)
          nv.validate_minmax_axis(axis)
  
          if not len(self):
              return self._na_value
  
          if len(self) and self.is_monotonic_increasing:
              # quick check
              first = self[0]
              if not isna(first):
                  return first
  
          if not self._is_multi and self.hasnans:
              # Take advantage of cache
              mask = self._isnan
              if not skipna or mask.all():
                  return self._na_value
  
          if not self._is_multi and not isinstance(self._values, np.ndarray):
              # "ExtensionArray" has no attribute "min"
              return self._values.min(skipna=skipna)  # type: ignore[attr-defined]
  
          return super().min(skipna=skipna)
  
      @doc(IndexOpsMixin.max)
      def max(self, axis=None, skipna=True, *args, **kwargs):
          nv.validate_max(args, kwargs)
          nv.validate_minmax_axis(axis)
  
          if not len(self):
              return self._na_value
  
          if len(self) and self.is_monotonic_increasing:
              # quick check
              last = self[-1]
              if not isna(last):
                  return last
  
          if not self._is_multi and self.hasnans:
              # Take advantage of cache
              mask = self._isnan
              if not skipna or mask.all():
                  return self._na_value
  
          if not self._is_multi and not isinstance(self._values, np.ndarray):
              # "ExtensionArray" has no attribute "max"
              return self._values.max(skipna=skipna)  # type: ignore[attr-defined]
  
          return super().max(skipna=skipna)
  
      # --------------------------------------------------------------------
  
      @final
      @property
      def shape(self) -> Shape:
          """
          Return a tuple of the shape of the underlying data.
          """
          # See GH#27775, GH#27384 for history/reasoning in how this is defined.
          return (len(self),)
  
      @final
      def _deprecated_arg(self, value, name: str_t, methodname: str_t) -> None:
          """
          Issue a FutureWarning if the arg/kwarg is not no_default.
          """
          if value is not no_default:
              warnings.warn(
                  f"'{name}' argument in {methodname} is deprecated "
                  "and will be removed in a future version.  Do not pass it.",
                  FutureWarning,
                  stacklevel=find_stack_level(),
              )
  
  
  def ensure_index_from_sequences(sequences, names=None) -> Index:
      """
      Construct an index from sequences of data.
  
      A single sequence returns an Index. Many sequences returns a
      MultiIndex.
  
      Parameters
      ----------
      sequences : sequence of sequences
      names : sequence of str
  
      Returns
      -------
      index : Index or MultiIndex
  
      Examples
      --------
      >>> ensure_index_from_sequences([[1, 2, 3]], names=["name"])
      Int64Index([1, 2, 3], dtype='int64', name='name')
  
      >>> ensure_index_from_sequences([["a", "a"], ["a", "b"]], names=["L1", "L2"])
      MultiIndex([('a', 'a'),
                  ('a', 'b')],
                 names=['L1', 'L2'])
  
      See Also
      --------
      ensure_index
      """
      from pandas.core.indexes.multi import MultiIndex
  
      if len(sequences) == 1:
          if names is not None:
              names = names[0]
          return Index(sequences[0], name=names)
      else:
          return MultiIndex.from_arrays(sequences, names=names)
  
  
  def ensure_index(index_like: AnyArrayLike | Sequence, copy: bool = False) -> Index:
      """
      Ensure that we have an index from some index-like object.
  
      Parameters
      ----------
      index_like : sequence
          An Index or other sequence
      copy : bool, default False
  
      Returns
      -------
      index : Index or MultiIndex
  
      See Also
      --------
      ensure_index_from_sequences
  
      Examples
      --------
      >>> ensure_index(['a', 'b'])
      Index(['a', 'b'], dtype='object')
  
      >>> ensure_index([('a', 'a'),  ('b', 'c')])
      Index([('a', 'a'), ('b', 'c')], dtype='object')
  
      >>> ensure_index([['a', 'a'], ['b', 'c']])
      MultiIndex([('a', 'b'),
              ('a', 'c')],
             )
      """
      if isinstance(index_like, Index):
          if copy:
              index_like = index_like.copy()
          return index_like
  
      if isinstance(index_like, ABCSeries):
          name = index_like.name
          return Index._with_infer(index_like, name=name, copy=copy)
  
      if is_iterator(index_like):
          index_like = list(index_like)
  
      if isinstance(index_like, list):
          if type(index_like) is not list:
              # must check for exactly list here because of strict type
              # check in clean_index_list
              index_like = list(index_like)
  
          if len(index_like) and lib.is_all_arraylike(index_like):
              from pandas.core.indexes.multi import MultiIndex
  
              return MultiIndex.from_arrays(index_like)
          else:
              return Index._with_infer(index_like, copy=copy, tupleize_cols=False)
      else:
          return Index._with_infer(index_like, copy=copy)
  
  
  def ensure_has_len(seq):
      """
      If seq is an iterator, put its values into a list.
      """
      try:
          len(seq)
      except TypeError:
          return list(seq)
      else:
          return seq
  
  
  def trim_front(strings: list[str]) -> list[str]:
      """
      Trims zeros and decimal points.
  
      Examples
      --------
      >>> trim_front([" a", " b"])
      ['a', 'b']
  
      >>> trim_front([" a", " "])
      ['a', '']
      """
      if not strings:
          return strings
      while all(strings) and all(x[0] == " " for x in strings):
          strings = [x[1:] for x in strings]
      return strings
  
  
  def _validate_join_method(method: str) -> None:
      if method not in ["left", "right", "inner", "outer"]:
          raise ValueError(f"do not recognize join method {method}")
  
  
  def maybe_extract_name(name, obj, cls) -> Hashable:
      """
      If no name is passed, then extract it from data, validating hashability.
      """
      if name is None and isinstance(obj, (Index, ABCSeries)):
          # Note we don't just check for "name" attribute since that would
          #  pick up e.g. dtype.name
          name = obj.name
  
      # GH#29069
      if not is_hashable(name):
          raise TypeError(f"{cls.__name__}.name must be a hashable type")
  
      return name
  
  
  _cast_depr_msg = (
      "In a future version, passing an object-dtype arraylike to pd.Index will "
      "not infer numeric values to numeric dtype (matching the Series behavior). "
      "To retain the old behavior, explicitly pass the desired dtype or use the "
      "desired Index subclass"
  )
  
  
  def _maybe_cast_data_without_dtype(
      subarr: np.ndarray, cast_numeric_deprecated: bool = True
  ) -> ArrayLike:
      """
      If we have an arraylike input but no passed dtype, try to infer
      a supported dtype.
  
      Parameters
      ----------
      subarr : np.ndarray[object]
      cast_numeric_deprecated : bool, default True
          Whether to issue a FutureWarning when inferring numeric dtypes.
  
      Returns
      -------
      np.ndarray or ExtensionArray
      """
  
      result = lib.maybe_convert_objects(
          subarr,
          convert_datetime=True,
          convert_timedelta=True,
          convert_period=True,
          convert_interval=True,
          dtype_if_all_nat=np.dtype("datetime64[ns]"),
      )
      if result.dtype.kind in ["i", "u", "f"]:
          if not cast_numeric_deprecated:
              # i.e. we started with a list, not an ndarray[object]
              return result
  
          warnings.warn(
              "In a future version, the Index constructor will not infer numeric "
              "dtypes when passed object-dtype sequences (matching Series behavior)",
              FutureWarning,
              stacklevel=3,
          )
      if result.dtype.kind in ["b", "c"]:
          return subarr
      result = ensure_wrapped_if_datetimelike(result)
      return result
  
  
  def get_unanimous_names(*indexes: Index) -> tuple[Hashable, ...]:
      """
      Return common name if all indices agree, otherwise None (level-by-level).
  
      Parameters
      ----------
      indexes : list of Index objects
  
      Returns
      -------
      list
          A list representing the unanimous 'names' found.
      """
      name_tups = [tuple(i.names) for i in indexes]
      name_sets = [{*ns} for ns in zip_longest(*name_tups)]
      names = tuple(ns.pop() if len(ns) == 1 else None for ns in name_sets)
      return names
  
  
  def unpack_nested_dtype(other: _IndexT) -> _IndexT:
      """
      When checking if our dtype is comparable with another, we need
      to unpack CategoricalDtype to look at its categories.dtype.
  
      Parameters
      ----------
      other : Index
  
      Returns
      -------
      Index
      """
      dtype = other.dtype
      if is_categorical_dtype(dtype):
          # If there is ever a SparseIndex, this could get dispatched
          #  here too.
          # error: Item  "dtype[Any]"/"ExtensionDtype" of "Union[dtype[Any],
          # ExtensionDtype]" has no attribute "categories"
          return dtype.categories  # type: ignore[union-attr]
      return other
  
  
  def _maybe_try_sort(result, sort):
      if sort is None:
          try:
              result = algos.safe_sort(result)
          except TypeError as err:
              warnings.warn(
                  f"{err}, sort order is undefined for incomparable objects.",
                  RuntimeWarning,
                  stacklevel=find_stack_level(),
              )
      return result
  