You're thinking of hybrid properties(see the docs). These can be used to exhibit different behaviour in SQL vs python settings, but can also be used to predefine certain transformations. I've often used them to transform UTC timestamps to the local timezone. Note that you define the property 1-3 times. Once as a python property, once for how you would like the SQL to function, and once for a setter.
import pytz
from sqlalchemy.ext.hybrid import hybrid_property
class Appointment(Base):
    scheduled_date_utc = Column(DateTime)               # Naive UTC 
    scheduled_date_timezone = Column(TimezoneType())    # TimezoneType is from sqlalchemy-utils
    @property
    def scheduled_date(self) -> datetime:
        # see https://stackoverflow.com/a/18646797/5015356
        return self.scheduled_date_utc\
          .replace(tzinfo=pytz.utc)\
          .astimezone(pytz.timezone(self.scheduled_date_timezone))
    @scheduled_date.expr
    def scheduled_date(cls):
        return func.timezone(cls.scheduled_date_timezone, cls.scheduled_date_utc)
To make the solution reusable, you could write a mixin with a wrapper around __setattr__:
import pytz
class TimeZoneMixin:
    def is_timezone_aware_attr(self, attr):
        return hasattr(self, attr + '_utc') and hasattr(self, attr + '_timezone')
    def __getattr__(self, attr):
        """
        __getattr__ is only called as a last resort, if no other
        matching columns exist
        """
        if self.is_timezone_aware_attr(attr):
            return func.timezone(getattr(self, attr + '_utc'),
                                 getattr(self, attr + '_timezone')) 
        raise AttributeError()
    def __setattr__(self, attr, value):
        if self.is_timezone_aware_attr(attr):
            setattr(self, attr + '_utc', value.astimezone(tzinfo=pytz.utc))
            setattr(self, attr + '_utc', value.tzinfo)
        raise AttributeError()
Or to make it use only one shared timezone object:
import pytz
class TimeZoneMixin:
    timezone = Column(TimezoneType())
    def is_timezone_aware_attr(self, attr):
        return hasattr(self, attr + '_utc')
    def __getattr__(self, attr):
        """
        __getattr__ is only called as a last resort, if no other
        matching columns exist
        """
        if self.is_timezone_aware_attr(attr):
            return func.timezone(getattr(self, attr + '_utc'), self.timezone) 
        raise AttributeError()
    def __setattr__(self, attr, value):
        if self.is_timezone_aware_attr(attr):
            setattr(self, attr + '_utc', value.astimezone(tzinfo=pytz.utc))
            self.timezone = value.tzinfo
        raise AttributeError()