How to create a read-only class property in Python?

This will make Foo.number a read-only property:

class MetaFoo(type):
    @property
    def number(cls):
        return cls.x

class Foo(object, metaclass=MetaFoo):
    x = 4

print(Foo.number)
# 4

Foo.number = 6
# AttributeError: can't set attribute

Explanation: The usual scenario when using @property looks like this:

class Foo(object):
    @property
    def number(self):
        ...
foo = Foo()

A property defined in Foo is read-only with respect to its instances. That is, foo.number = 6 would raise an AttributeError.

Analogously, if you want Foo.number to raise an AttributeError you would need to setup a property defined in type(Foo). Hence the need for a metaclass.


Note that this read-onlyness is not immune from hackers. The property can be made writable by changing Foo's class:

class Base(type): pass
Foo.__class__ = Base

# makes Foo.number a normal class attribute
Foo.number = 6   
print(Foo.number)

prints

6

or, if you wish to make Foo.number a settable property,

class WritableMetaFoo(type): 
    @property
    def number(cls):
        return cls.x
    @number.setter
    def number(cls, value):
        cls.x = value
Foo.__class__ = WritableMetaFoo

# Now the assignment modifies `Foo.x`
Foo.number = 6   
print(Foo.number)

also prints

6

The property descriptor always returns itself when accessed from a class (ie. when instance is None in its __get__ method).

If that's not what you want, you can write a new descriptor that always uses the class object (owner) instead of the instance:

>>> class classproperty(object):
...     def __init__(self, getter):
...         self.getter= getter
...     def __get__(self, instance, owner):
...         return self.getter(owner)
... 
>>> class Foo(object):
...     x= 4
...     @classproperty
...     def number(cls):
...         return cls.x
... 
>>> Foo().number
4
>>> Foo.number
4

I agree with unubtu's answer; it seems to work, however, it doesn't work with this precise syntax on Python 3 (specifically, Python 3.4 is what I struggled with). Here's how one must form the pattern under Python 3.4 to make things work, it seems:

class MetaFoo(type):
   @property
   def number(cls):
      return cls.x

class Foo(metaclass=MetaFoo):
   x = 4

print(Foo.number)
# 4

Foo.number = 6
# AttributeError: can't set attribute