I am writing an object-oriented app to help our developers manage some cloud systems. I’d like to make the configuration information available to all the classes, but I’m not sure of a good way to do that. Everything I can think of seems to fall under the category of “global variables” which as far as I know is a Very Bad Thing.
I already have a logging Mixin class that enables logging for every class that inherits it, and I was wondering if that’s the right way to approach the configuration data:
class LoggingMixin:
@classmethod
@property
def log(cls):
return logging.getLogger(cls.__name__)
class TestClassA(LoggingMixin):
def testmethod1(self):
self.log.debug("debug message from test class A")
if __name__ == "__main__":
logging.basicConfig(
format="{created:<f} {levelname:>5s} {name}.{funcName:>8s} |{message}|",
level=logging.DEBUG,
style="{",
)
a = TestClassA()
a.testmethod1()
Outputs (in case you are curious)
1688494741.449282 DEBUG TestClassA.testmethod1 |debug message from test class A|
What’s a good way of making data from a class available to all classes/objects? It wouldn’t be static, it’d be combined from a JSON file and any command line parameters.
If I copied the example above but changed it to a ConfigMixin, would that work? With the logging example, each class creates its own logger object when it first calls self.log.debug
, so that might not work because each object needs to get the same config data.
Is there a pattern or other design that could help? How do you make configuration data available to your whole app? Do you have a config object that can get/set values and saves to disk, etc?
Thank you for reading, my apologies for poorly worded questions.
You need traitlets yesterday. It comes with a logging attribute and has extensive support for configuration - via command-line, or config files or python oop.
Ok now those look really useful! You are an excellent person and I appreciate you.
Hey, I implemented the config system using traitlets and it works great! Thank you again!
You have the right idea.
One approach is to store cfg in json w/a class that reads that json. Remember to validate the json vs schema first!
That cfg class should also set env vars so that you don’t need to rely on env vars being correctly set by an invoking shell (json to rescue).
The cfg object instance becomes a parameter to class constructors or module functions.
If the idea of passing around a cfg instance is appalling, then hello global / singleton pattern!
In programming, people often talk in absolutes, but there‘s often exceptions to these rules, depending on situation and depending on the programming language. In Python, things like dependency injection don‘t make a lot of sense, because it just isn‘t needed. It‘s a pattern extensively used in other languages like Java, but only because Java has limitations that don‘t exist in Python. Trying to use Java patterns into Python just because it „sounds right“ or because it‘s a „best practice“ in another language does not make your code actually good python. It‘s not necessarily bad, but more complicated than it needs to be, and simpler is usually better if it yields the same end result. Don‘t overthink it.
You say global variables are a very bad thing, but do you know why that is? People often hear about things like that but don‘t learn about the „why“, which hinders you from properly thinking about your code.
Global variables are a bad thing because it is very hard to reason about their state at certain points in your programs execution. But what if your variable never changes, like a constant? Then you don‘t really have that problem. So an immutable config read in at startup and accessed by multiple classes/functions works just fine. You could also have a function that, when you run it the first time loads the config, „caches“ it in a global variable and then in subsequent calls just returns the global config.
To ensure immutability, you can use a frozen dataclass or something with validation built in like frozen pydantic models.
If you think you need mutability, think again. It’s better to put whatever you need to mutate outside of the global Config class, and then initialize that with the config. But keep the config itself read-only.
Agree.
Globals are a big problem in multithreaded / shared memory envs even when designs have a clear “here is where globals are set, and this is when”.
If single threaded, it’s a style choice but can lead to subtle bugs when the global keyword is forgotten - for ex, a function is accessing a global variable without the global keyword.
Then, later on, a developer updates the function to store a result in the variable, which converts the variable to local. Suddenly code which has been working “for years” may suddenly be askew and it’s not necessarily clear why (a good reason to lint code imo).
So if using global, be diligent in identifying them as such! Have a style standard (g_<var name> to help.
This is probably a good point to add that I do try to generally avoid globals as well. I am a fan of limiting mutable state in general. But for constants (whether they are hard coded in the source code or read from a file doesn’t really matter to me) they make sense.
And just to emphasize again, if you are building an App with user settings that may be changed during runtime, the „global constant config“ pattern does not apply. It only really works for static configuration read at startup.
For a mutable runtime config, you really have to think hard about when it is allowed to change and how these changes are propagated throughout your app, otherwise you may end up with inconsistent behavior where part of your program still uses the old values. There’s many ways to solve this depending on the programs architecture and requirements though.
Ah. Great point. In a threaded environment with dynamic configuration, paying attention to the exact use case is essential! Hidden constraints and unknown requirements might undo generic advice. That said, publisher/subscriber is a decent async pattern!