In Rust, the tests usually sit right next to the source code, even in the same file. That’s partly because the compiler can just strip the tests from the final binary, and I assume partly just conventional. In Python, you usually want to keep the tests out of the final sdist/wheel, so the setup you described is probably the most common in bigger projects.
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.
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.
Oh my gosh... This project and its readme are amazing. I gotta try this at work tomorrow, I hope my co-workers can bear with me.
Edit: I just found the release notes... I love it