Best practice: Setter and getter vs passing arg to constructor

Dear Experts,

When instantiating classes I see three cases:

  1. Little config: If I have 3 or 4 pieces of information to configure he class I normally pass them to the constructor.
  2. Middle config: If I have more than 5 pieces of information, I sometimes tend to use the @property decorator to set and get this information to and from protected attributes.
  3. Large config: If there are more than 10 I tend to just put them all in a toml config file and I only pass the version of the file to the class.

How do you deal with building instances of classes and the fact that, depending on the class, you might need few or many ingredients to build it?


There is another factor to consider: a typical object (instance of a class) has ‘invariants’, conditions which must be met for its operations to be usable. If the data required to satisfy those invariants is not provided as constructor arguments, the object will be in an inconsistent state and any attempt to invoke those operations could result in unexpected behavior.


personally I’m a big fan of @dataclass, which creates the constructor for you. It makes it viable to pass up to 10 pieces of information explicitly. You can then also do the sanity checking @kpfleming mentioned with post_init. And you can use default values and default factory functions to make your life easier.

Also I personally usually use @cached_property, and rarely a plain @property.


If there are a lot of pieces of information in the class, organize it hierarchically: use more classes to represent portions of the information. If you have some config TOML then this may suggest a class breakdown naturally (based on how the TOML is structured). This way, you never have “big” classes (unless you mean in terms of total memory usage).

I recommend not writing constructors that expect a TOML config (whether that’s a file name, open file, already-parsed data…). That makes things more awkward if you ever want to instantiate the class a different way. Instead, either do the work outside the class to extract “ordinary” constructor parameters from the TOML, or use a “factory” @classmethod or @staticmethod (according to your requirements) to put that work inside the class. It will generally be easier to express the TOML creation logic in terms of “ordinary” creation logic, than the other way around.

1 Like

Dear Peter,

Thanks for your feedback on @dataclass and @cached_property. I had heard of the former but not of the latter. It makes sense the use of it instead of just @property, given that it seems that once you use it, you set the value and if you use it again, the second invocation does not change the value. That would prevent mistakes where you set the value a second time incorrectly. I imagine that’s the point of this tool.

Also, when I need to cache something, I usually put it in a protected class attribute, this would hide that and make the code cleaner. However, sometimes I do cache stuff in JSON files in code that looks like:

def get_result():
    if os.path.isfile('data.json'):
        data = json.loads('data.json')
        return data

    #here lines that calculate data
    with open('data.json', 'w') as ofile:
        json.dumps(ofile, data)
    return data

because that data is hard to calculate and I might not need to update it often. That could really be simplified with a:

def get_result():
    #here lines that calculate data
    return data

I wonder if that exists, if not I am writing it and adding it to my private library :slight_smile:


I’m not fully understanding what you mean.

To start with the last point, because I think that is clearest. it looks like you want @functools.cache. Though @functools.lru_cache(1) can also be useful, there are cases where you’d want lru_cache(N) with N some other number, and some people prefer to use @lru_cache() instead of @cache.
I’m not aware of a decorator that caches only a specified input or only the first input unfortunately; such a decorator would be useful.

On the subject of @[cached]property, I realise my initial advice may have been a bit misleading.
You need to ask the question why you’re using getters and setters instead of using the raw attributes.
A setter is useful if you want to warn a user when they set a class attribute to an illegal state. But I think for the most part that that’s redundant. Usually you yourself are the user, or the object isn’t expected to be directly manipulated by the user, or anyone who changes the attributes of an object directly (and incorrectly) deserves to get their fingers burned.
If a @property can be changed by a setter, using @cachedproperty is asking for bugs.
And if the @cachedproperty shouldn’t be changed at any point, it’s perfectly permissible to set the _hidden_attribute directly in your factory, without creating a setter to do so.

@cachedproperty is useful for situations like these:

(from the functools documentation:)

class DataSet:

    def __init__(self, sequence_of_numbers):
        self._data = tuple(sequence_of_numbers)

    def stdev(self):
        return statistics.stdev(self._data)


from dataclasses import dataclass
from functools import cached_property

class Rectangle:
  width: int = 3
  height: int = 2

  def area(self):
    return self.width * self.height

  def shape(self):
    return (self.width, self.height)
square = Rectangle(3, 3)
assert square.area == 9
assert square.shape == (3, 3)
assert square == Rectangle(height=3)

Dear Peter,

Yes, that’s clearer now, thank you for your time and the pointers to these useful tools. Specially the part about @cache_property, I also went through the documentation.

However, I think there was a misunderstanding again. The @functools.cache decorator et al are caching to memory, right? I mean, when the program ends, whatever is cached, will be no more available.

As a user, what I have tried to implement are caching lines that store data in JSON files. That way the next run of the program would just pick up the hard to compute data from those files. That in my case speeds up things and if I need to update the cached data, I usually just run with a -u flag, to update the cached stuff. I do not think this sort of functionality exists as a functor yet, at least not in the standard library.


Indeed I don’t think that kind of caching is implemented in a standard library. I also can’t see a library making a good enough guess about what kind of behaviour a user needs here.

If this is a pattern you use often I would recommend writing your own decorator though. The thing to realise is that @decorator; def f(): ... is shorthand for def f(): ... ; f = decorator(f). And

def f():...

is shorthand for

def f():... 
f = decorator(specs)(f)

Persisting a Cache in Python to Disk using a decorator shows how you can do something vaguely like you want. You can adapt from there.


Dear @kpfleming ,

Thanks for your feedback. My comment was specially referring to situations where we have classes that need many inputs to be built. Just now I am going through code written by a colleague that has a constructor with 57 arguments. I have the feeling that is not ideal and if I had done this, I would put all those flags and strings in a config file and I would be passing only a string pointing the class to the TOML file where the configuration is stored. However, it is not clear if this is what professional developers, with experience do.


If that class is a singleton (one instance in the process) used for controlling the behavior of the rest of the program, then yes, gathering its input from some sort of configuration file makes a lot of sense. 57 arguments is about as far as from ‘ideal’ as you can get :slight_smile:

1 Like