36

Is there some way to make a class-level read-only property in Python? For instance, if I have a class Foo, I want to say:

x = Foo.CLASS_PROPERTY

but prevent anyone from saying:

Foo.CLASS_PROPERTY = y

EDIT: I like the simplicity of Alex Martelli's solution, but not the syntax that it requires. Both his and ~unutbu's answers inspired the following solution, which is closer to the spirit of what I was looking for:

class const_value (object):
    def __init__(self, value):
        self.__value = value

    def make_property(self):
        return property(lambda cls: self.__value)

class ROType(type):
    def __new__(mcl,classname,bases,classdict):
        class UniqeROType (mcl):
            pass

        for attr, value in classdict.items():
            if isinstance(value, const_value):
                setattr(UniqeROType, attr, value.make_property())
                classdict[attr] = value.make_property()

        return type.__new__(UniqeROType,classname,bases,classdict)

class Foo(object):
    __metaclass__=ROType
    BAR = const_value(1)
    BAZ = 2

class Bit(object):
    __metaclass__=ROType
    BOO = const_value(3)
    BAN = 4

Now, I get:

Foo.BAR
# 1
Foo.BAZ
# 2
Foo.BAR=2
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: can't set attribute
Foo.BAZ=3
#

I prefer this solution because:

  • The members get declared inline instead of after the fact, as with type(X).foo = ...
  • The members' values are set in the actual class's code as opposed to in the metaclass's code.

It's still not ideal because:

  • I have to set the __metaclass__ in order for const_value objects to be interpreted correctly.
  • The const_values don't "behave" like the plain values. For example, I couldn't use it as a default value for a parameter to a method in the class.
5
  • 3
    Why would anyone ever try to set Foo.CLASS_PROPERTY? It's typical style in Python to choose simplicity over planning for preventing a theoretical instance where someone does something stupid. There isn't anything you can do to keep people from doing stupid stuff with your code. Commented Nov 14, 2009 at 23:15
  • 1
    @Mike: You're right; common sense would tell people to not modify these properties. And yet, occasionally, they get modified. That's why I like for it to be spelled out (more explicitly than with all-caps). Same rationale as using private members. Explicit interfaces are good. Don't get me wrong: I like simplicity too. I'd much prefer if there were some built-in facility for this (like a @classproperty decorator or something). As it is, I'm not even sure I would use the above solution if it requires setting the __metaclass__ each time. All-caps may win out, for now. Commented Nov 15, 2009 at 0:07
  • 2
    Note to people from the future: Modern python versions can use @property in new-style classes: docs.python.org/library/functions.html#property Commented Jun 7, 2012 at 15:12
  • 1
    I think it's counter to the intent of stack-exchange to edit your question and insert an answer. Instead I'd suggest that if you have a better answer to just write an answer among all other answers. There's nothing preventing you from answering your own question, you can even accept that answer after a while. Commented Oct 27, 2018 at 8:53
  • @mattb As far as I know that will only create object level readonly attributes. The question was to create class level readonly attributes. Commented Oct 27, 2018 at 8:54

4 Answers 4

10

The existing solutions are a bit complex -- what about just ensuring that each class in a certain group has a unique metaclass, then setting a normal read-only property on the custom metaclass. Namely:

>>> class Meta(type):
...   def __new__(mcl, *a, **k):
...     uniquemcl = type('Uniq', (mcl,), {})
...     return type.__new__(uniquemcl, *a, **k)
... 
>>> class X: __metaclass__ = Meta
... 
>>> class Y: __metaclass__ = Meta
... 
>>> type(X).foo = property(lambda *_: 23)
>>> type(Y).foo = property(lambda *_: 45)
>>> X.foo
23
>>> Y.foo
45
>>> 

this is really much simpler, because it's based on nothing more than the fact that when you get an instance's attribute descriptors are looked up on the class (so of course when you get a class's attribute descriptors are looked on the metaclass), and making class/metaclass unique isn't terribly hard.

Oh, and of course:

>>> X.foo = 67
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

just to confirm it IS indeed read-only!

Sign up to request clarification or add additional context in comments.

Comments

5

The ActiveState solution that Pynt references makes instances of ROClass have read-only attributes. Your question seems to ask if the class itself can have read-only attributes.

Here is one way, based on Raymond Hettinger's comment:

#!/usr/bin/env python
def readonly(value):
    return property(lambda self: value)

class ROType(type):
    CLASS_PROPERTY = readonly(1)

class Foo(object):
    __metaclass__=ROType

print(Foo.CLASS_PROPERTY)
# 1

Foo.CLASS_PROPERTY=2
# AttributeError: can't set attribute

The idea is this: Consider first Raymond Hettinger's solution:

class Bar(object):
    CLASS_PROPERTY = property(lambda self: 1)
bar=Bar()
bar.CLASS_PROPERTY=2

It shows a relatively simple way to give bar a read-only property.

Notice that you have to add the CLASS_PROPERTY = property(lambda self: 1) line to the definition of the class of bar, not to bar itself.

So, if you want the class Foo to have a read-only property, then the parent class of Foo has to have CLASS_PROPERTY = property(lambda self: 1) defined.

The parent class of a class is a metaclass. Hence we define ROType as the metaclass:

class ROType(type):
    CLASS_PROPERTY = readonly(1)

Then we make Foo's parent class be ROType:

class Foo(object):
    __metaclass__=ROType

Comments

0

Found this on ActiveState:

# simple read only attributes with meta-class programming

# method factory for an attribute get method
def getmethod(attrname):
    def _getmethod(self):
        return self.__readonly__[attrname]

    return _getmethod

class metaClass(type):
    def __new__(cls,classname,bases,classdict):
        readonly = classdict.get('__readonly__',{})
        for name,default in readonly.items():
            classdict[name] = property(getmethod(name))

        return type.__new__(cls,classname,bases,classdict)

class ROClass(object):
    __metaclass__ = metaClass
    __readonly__ = {'a':1,'b':'text'}


if __name__ == '__main__':
    def test1():
        t = ROClass()
        print t.a
        print t.b

    def test2():
        t = ROClass()
        t.a = 2

    test1()

Note that if you try to set a read-only attribute (t.a = 2) python will raise an AttributeError.

1 Comment

This works for instance level properties, but not class-level (i.e. I still couldn't say ROClass.a and expect the value to be 1).
0

This is an old question, but for anyone who happens to stumble on this anew, the preferred way to solve this seems to be using the @property decorator.

Docs: https://docs.python.org/3/library/functions.html#property

Using OP's example:

class Foo:

    def __init__(self, bar: str):
        self.bar = bar
        self._baz = "BAZ!"

    @property
    def baz(self):
        """Return the value of baz"""
        return self._baz


f = Foo(bar="bar")

print(f.bar)
f.bar = "boop"
print(f.bar)

print(f.baz)
f.baz = "beep"

Output:

$ python example.py
bar
boop
BAZ!
Traceback (most recent call last):
  File "example.py", line 20, in <module>
    f.baz = "beep"
AttributeError: can't set attribute 'baz'

2 Comments

The caveat is that someone can technically work around this by just setting self._baz directly, as nothing prevents that. It's just convention that you leave variables/attributes with a single preceding underscore alone. If the value of baz is meant to be truly read only, and it is the same for every instance of the class, then the property method should simply return the constant value "BAZ!" instead of self._baz
This is not class level.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.