from django.db.models.signals import post_save

class DirtyFieldsMixin(object):
    def __init__(self, *args, **kwargs):
        super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
        post_save.connect(reset_state, sender=self.__class__, 
                            dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
        reset_state(sender=self.__class__, instance=self)

    def _as_dict(self):
        return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
    
    def get_dirty_fields(self):
        new_state = self._as_dict()
        return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
    
    def is_dirty(self):
        # in order to be dirty we need to have been saved at least once, so we
        # check for a primary key and we need our dirty fields to not be empty
        if not self.pk: 
            return True
        return {} != self.get_dirty_fields()

def reset_state(sender, instance, **kwargs):
    instance._original_state = instance._as_dict()

from django.db.models.signals import post_save

class DirtyFieldsMixin(object):
    def __init__(self, *args, **kwargs):
        super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
        post_save.connect(reset_state, sender=self.__class__, 
                            dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
        reset_state(sender=self.__class__, instance=self)

    def _as_dict(self):
        return dict([(f.name, f.to_python(getattr(self, f.name))) for f in self._meta.local_fields if not f.rel])
    
    def get_dirty_fields(self):
        new_state = self._as_dict()
        return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
    
    def is_dirty(self):
        # in order to be dirty we need to have been saved at least once, so we
        # check for a primary key and we need our dirty fields to not be empty
        if not self.pk: 
            return True
        return {} != self.get_dirty_fields()

def reset_state(sender, instance, **kwargs):
    instance._original_state = instance._as_dict()

Use field.to_python to do django type conversions on the field before checking if dirty.
Use field.to_python to do django type conversions on the field before checking if dirty.

This solves issues where you might have a decimal field that you write a string to, eg:
>>> m = MyModel.objects.get(id=1)
>>> m.my_decimal_field
Decimal('1.00')
>>> m.my_decimal_field = u'1.00' # from a form or something
>>> m.is_dirty() # currently evaluates to True, should evaluate to False
False

This pull request could probably use some unit testing, but it should be safe as the base class for django fields defines to_python as:

def to_python(self, value):
  return value

So, any field type that does not have an explicit to_python method will behave as before this change.
  from django.db.models.signals import post_save
  
  class DirtyFieldsMixin(object):
      def __init__(self, *args, **kwargs):
          super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
          post_save.connect(reset_state, sender=self.__class__, 
                              dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
          reset_state(sender=self.__class__, instance=self)
  
      def _as_dict(self):
-         return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
+         return dict([(f.name, f.to_python(getattr(self, f.name))) for f in self._meta.local_fields if not f.rel])
      
      def get_dirty_fields(self):
          new_state = self._as_dict()
          return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
      
      def is_dirty(self):
          # in order to be dirty we need to have been saved at least once, so we
          # check for a primary key and we need our dirty fields to not be empty
          if not self.pk: 
              return True
          return {} != self.get_dirty_fields()
  
  def reset_state(sender, instance, **kwargs):
      instance._original_state = instance._as_dict()
  
Use field.to_python to do django type conversions on the field before checking if dirty.
## Code Before:
from django.db.models.signals import post_save

class DirtyFieldsMixin(object):
    def __init__(self, *args, **kwargs):
        super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
        post_save.connect(reset_state, sender=self.__class__, 
                            dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
        reset_state(sender=self.__class__, instance=self)

    def _as_dict(self):
        return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
    
    def get_dirty_fields(self):
        new_state = self._as_dict()
        return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
    
    def is_dirty(self):
        # in order to be dirty we need to have been saved at least once, so we
        # check for a primary key and we need our dirty fields to not be empty
        if not self.pk: 
            return True
        return {} != self.get_dirty_fields()

def reset_state(sender, instance, **kwargs):
    instance._original_state = instance._as_dict()

## Instruction:
Use field.to_python to do django type conversions on the field before checking if dirty.
## Code After:
from django.db.models.signals import post_save

class DirtyFieldsMixin(object):
    def __init__(self, *args, **kwargs):
        super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
        post_save.connect(reset_state, sender=self.__class__, 
                            dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
        reset_state(sender=self.__class__, instance=self)

    def _as_dict(self):
        return dict([(f.name, f.to_python(getattr(self, f.name))) for f in self._meta.local_fields if not f.rel])
    
    def get_dirty_fields(self):
        new_state = self._as_dict()
        return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
    
    def is_dirty(self):
        # in order to be dirty we need to have been saved at least once, so we
        # check for a primary key and we need our dirty fields to not be empty
        if not self.pk: 
            return True
        return {} != self.get_dirty_fields()

def reset_state(sender, instance, **kwargs):
    instance._original_state = instance._as_dict()
