None of the answers mention the performance impact of overriding __setattr__, which can be an issue when creating many small objects. (And __slots__ would be the performant solution but limits pickle/inheritance).
So I came up with this variant which installs our slower settatr after init:
class FrozenClass:
    def freeze(self):
        def frozen_setattr(self, key, value):
            if not hasattr(self, key):
                raise TypeError("Cannot set {}: {} is a frozen class".format(key, self))
            object.__setattr__(self, key, value)
        self.__setattr__ = frozen_setattr
class Foo(FrozenClass): ...
If you don't want to call freeze at the end of __init__, if inheritance is an issue, or if you don't want it in vars(), it can also be adapted: for example here is a decorator version based on the pystrict answer:
import functools
def strict(cls):
    cls._x_setter = getattr(cls, "__setattr__", object.__setattr__)
    cls._x_init = cls.__init__
    @functools.wraps(cls.__init__)
    def wrapper(self, *args, **kwargs):
        cls._x_init(self, *args, **kwargs)
        def frozen_setattr(self, key, value):
            if not hasattr(self, key):
                raise TypeError("Class %s is frozen. Cannot set '%s'." % (cls.__name__, key))
            cls._x_setter(self, key, value)
        cls.__setattr__ = frozen_setattr
    cls.__init__ = wrapper
    return cls
@strict
class Foo: ...