How to control recursion on a custom `__getitem__` method in a dataclass?

I have a dataclass, it carries an INITIALIZED state, and I only want to have users to access it if it’s in INITIALIZED state. The way that’s implemented at the moment is:

@dataclass
class MyControlledDataclass:
    _INITIALIZED: bool = False
    RANDOM_ATTR: str = "random"

    def __getattribute__(self, key):
        print("Im getting an attribute") # Line 7
        if not self._INITIALIZED:
            raise Exception("This class was not initialized")
        try:
            return getattr(self, key)
        except KeyError:
            print("Couldn't find Key!")
            raise AttributeError(key)

    def __getitem__(self, key):
        print("Im getting an item")
        is_initialized = getattr(self, "_INITIALIZED")
        if not is_initialized:
            raise Exception("initialized")
        try:
            return getattr(self, key)
        except KeyError:
            print("Couldn't find Key!")
            raise AttributeError(key)

And then I actually Initialize the object like:

controlled_class = MyControlledDataclass()

# After going through the initializer I set `_INITITLIZED=True`
initializer(controlled_class) 

And now, if I want to get an item, like controlled_class.RANDOM_ATTR I would expect this to go through the __getattribute__ method, so it can first check whether this was actually initialized or not.

No if I set the breakpoint at line 7 (the print on __getattribute__), before I move to line 8, the console already shows the recursion, and “im getting an attribute” has already printed multiple times (recursion limit). So I’m lost on what exactly is happening. Shouldn’t I be able to control the access through the __getattribute__? It seems to run even before I move to the next line. The same thing happens with __getitem__.

# With _INITILIZED = True
# This should work normally
"random" == controlled_class.RANDOM_ATTR

# With _INITILIZED = False
# This should raise a custom Exception saying the class was not Initialized
controlled_class.RANDOM_ATTR

# With _INITILIZED = True
# This should raise an AttributeError (it is initialized, but the key doesn't exist)
controlled_class.DONT_EXIST

I could use @property all around, but I have over 100 items, so not really optimal.
Ideas on what I might be doing wrong? Or workarounds?
I’m on Python 3.10.6

First off, this is an antipattern - a half-constructed object shouldn’t exist. So hopefully you have a good reason for needing this two-phase initialization. (One possible alternative would be a factory function which constructs the object and doesn’t return it until the initialization is complete, and that’s assuming you can’t simply initialize it in __init__.)

But in order to change the behaviour of __getattribute__, you have to carefully ensure that you never actually do attribute lookup. The easiest way is to use super:

if not super().__getattribute__("_INITIALIZED"):
    raise Exception # should probably be a TypeError or ValueError or something
return super().__getattribute__(key) # no try/except needed here

Alternatively, you can peek into self.__dict__ instead. That may be simpler for your purposes.

Hi, thanks for the response.

I do understand this is an antipattern, the reason why I implemented it that way is that I wanted a set of characteristics that I couldn’t find another way to handle:

(1) to have autocomplete on a settings class
(2) to be able to have a direct import of the object (from app.config import settings)
(3) to give flexibility on how it’s accessed (both as a dictionary-style syntax and a class-style syntax)
(4) to have controlled access to the keys
(5) to not have to write 100’s of @property.

I’ve tried different architectures, but that was the only one I could come up with that would allow me to have those characteristics (I’m open to suggestions though)

So I have a class Config object (which the object in question), and I initialize it empty, and then I fill it based on a .toml file.

I think what I was getting wrong was: using __getattr__ always (which would put me in a recursion) and not using the super() for the __getattribute__. It seems dataclasses don’t have a __getattr__ but do have a __getattribute__.

One weird thing that happened was that for some reason it does give me an error to try to access a __iter__ attribute (I guess this is related to the __getitem__? Anyways, by modifying this example slightly I could make this work:

@dataclass
class MyControlledConfig(object):
    _INITIALIZED: bool = False

    APP_PASS: str = ""
    APP_DATAFS: str = ""

    def __getattr__(self, key):
        print("Im getting an attribute")
        is_initialized = super().__getattribute__("_INITIALIZED")
        if not is_initialized:
            raise Exception("Settings were not intialized. Check your `settings.toml` file")
        try:
            return super().__getattribute__(key)
        except AttributeError:
            raise AttributeError(f"Setting {key} doesn't exist. Check your settings file.")

    def __getitem__(self, key):
        print("Im getting an item")
        is_initialized = super().__getattribute__("_INITIALIZED")
        if not is_initialized:
            raise Exception("Settings were not intialized. Check your `settings.toml` file")
        try:
            return super().__getattribute__(key)
        except AttributeError:
            raise AttributeError(f"Setting {key} doesn't exist. Check your settings file.")

    def __iter__(self):
        print("Why am I iterating things here?")
        pass

    def get(self, key):
        is_initialized = super().__getattribute__("_INITIALIZED")
        if not is_initialized:
            raise Exception("Settings were not intialized. Check your `settings.toml` file")
        return self.__getattr__(key)

I never really see the __iter__ being accessed, but I did see the message that the attribute was missing, don’t really know why.

I think that should work for now, I’ll keep investigating other implementations.
I also tried to make it a TypedDict so that the user would have autocomplete on the dict-styled syntax, but that doesn’t work because of the default empty fields (that are necessary for the dataclass initialization).

Thanks a lot!

Hmm, you can’t initialize it with the name of the toml file and have the __init__ method do the loading? Or do you need to initialize other things before getting to the actual loading? Anyhow.

Sounds reasonable. __getattr__ is only for missing attributes, and there’s no object.__getattr__, but you can always call __getattribute__ since it’s defined on object.

Yes, that sounds right. You could explicitly exclude any attributes that begin with an underscore, but “check for this attribute and do something else if it doesn’t exist” is a common idiom.