pandas-dev/pandas · error · TypeError

Cannot compare tz-naive and tz-aware datetime-like objects

Error message

Cannot compare tz-naive and tz-aware datetime-like objects

What it means

Mirror of [302]: DatetimeArray._assert_tzawareness_compat raises this variant (no trailing period in the message) when self is tz-aware but other is tz-naive. Same semantic rule — pandas will not silently compare across the awareness boundary. The two message variants exist for the two branches; the message inconsistency itself is a minor cosmetic bug.

Solutions

  1. Drop awareness from the aware side: `aware_obj.dt.tz_localize(None)` (keeps wall time) or `aware_obj.dt.tz_convert('UTC').dt.tz_localize(None)` (keeps UTC instant).
  2. Localize the naive side to match: `naive_obj.dt.tz_localize(aware_obj.dt.tz)`.
  3. Make the comparison reference tz-aware: `pd.Timestamp.now(tz=df['ts'].dt.tz)` instead of `pd.Timestamp.now()`.

Example fix

// before
mask = df['aware_ts'] < pd.Timestamp.now()

// after
mask = df['aware_ts'] < pd.Timestamp.now(tz=df['aware_ts'].dt.tz)
Defensive patterns

Strategy: validation

Validate before calling

import pandas as pd

def align_aware_to_naive_other(aware, other):
    other_tz = getattr(getattr(other, 'dtype', None), 'tz', getattr(other, 'tzinfo', None))
    if other_tz is None:
        if hasattr(other, 'dt'):
            other = other.dt.tz_localize(aware.dt.tz)
        elif hasattr(other, 'tz_localize'):
            other = other.tz_localize(aware.dt.tz)
    return aware, other

Type guard

def comparison_safe(aware, other) -> bool:
    other_tz = getattr(getattr(other, 'dtype', None), 'tz', getattr(other, 'tzinfo', None))
    return other_tz is not None

Try / catch

try:
    result = aware == other
except TypeError as e:
    if 'Cannot compare tz-naive and tz-aware' in str(e):
        other = pd.Timestamp(other).tz_localize(aware.dt.tz)
        result = aware == other
    else:
        raise

Prevention

When it happens

Trigger: Comparison or alignment where the left/own side is tz-aware and the right/other side is tz-naive. Concretely: `aware_dti == naive_ts`, `aware_series < naive_dti`, set ops between an aware index and a naive index.

Common situations: Default Timestamps (`pd.Timestamp.now()` is naive) compared against tz-aware columns loaded from a database with UTC offsets. Intersecting a tz-aware DatetimeIndex built via tz_localize with one constructed from naive strings.

Related errors


AI-assisted analysis of pandas-dev/pandas@3b7651241d (2026-08-11). Data as JSON: /api/errors/6e3805f65b7e107c. Report an issue: GitHub.

Appendix: source

Thrown at pandas/core/arrays/datetimes.py:786

    def _assert_tzawareness_compat(self, other) -> None:
        # adapted from _Timestamp._assert_tzawareness_compat
        other_tz = getattr(other, "tzinfo", None)
        other_dtype = getattr(other, "dtype", None)

        if isinstance(other_dtype, DatetimeTZDtype):
            # Get tzinfo from Series dtype
            other_tz = other.dtype.tz
        if other is NaT:
            # pd.NaT quacks both aware and naive
            pass
        elif self.tz is None:
            if other_tz is not None:
                raise TypeError(
                    "Cannot compare tz-naive and tz-aware datetime-like objects."
                )
        elif other_tz is None:
            raise TypeError(
                "Cannot compare tz-naive and tz-aware datetime-like objects"
            )

    # -----------------------------------------------------------------
    # Arithmetic Methods

    def _add_offset(self, offset: BaseOffset) -> Self:
        assert not isinstance(offset, Tick)

        # For pure-timedelta DateOffset with tz-aware data, add to UTC values
        # directly to avoid nonexistent/ambiguous time errors from
        # re-localizing wall-time results near DST (GH#28610).
        if (
            self.tz is not None
            and isinstance(offset, RelativeDeltaOffset)
            and not offset._use_relativedelta
        ):
            # GH#64806 _add_timedeltalike_scalar casts to the finer of the two

View on GitHub (pinned to 3b7651241d)