I am building a new Python library project to be consumed by several of my application projects. The existing code consumes environment variables for various configuration settings. Should my library project read configuration from environment variables? I've always learned that libraries shouldn't make assumptions about the environment they are running in. What is the best way to provide configuration to library projects from the applications/environments that consume them?
1 Answer
There is no single best way, because it depends on the architecture of the library. Reading environment variables directly in library code is also problematic for writing unit tests, which is another reason to avoid this if possible. This isn't a comprehensive list, but is a good start:
Create a "config" class that other classes or functions in your library can use. Consumers of your library can initialize this config object however they see fit.
Benefits:
- Consumers have a strongly-typed object with well-named properties.
- Dependency injection is required for your library to utilize this object, which makes testing easier as well.
Drawbacks:
- If your library is not designed for dependency injection then you might have a non-insignificant refactoring job ahead of you, which might require breaking changes to the API.
Create a "config" function that accepts all of the settings as arguments. This config function can then set variables or properties that are internal to your library.
Benefits:
- Consumers have a single point to configure the library.
- Might only require minor refactoring if your library was not initially designed for dependency injection.
Drawbacks:
- Unit testing your library might become a little harder.
- Many of the settings will be "static" rather than "instance" variables, making it impossible for consumers to have two sets of configurations.
- Although that is also a drawback for reading configs from env vars.
Add additional parameters to functions for those settings.
Benefits:
- You don't need environment variables anymore.
- Easier to test the library code with unit tests.
Drawbacks:
- Functions become more complicated to call because of the additional arguments.
- Potentially lots of breaking changes for consumers.
Combine options 1 and 2. This can be an intermediate step if you want to give clients a clean interface for configuring your library while you refactor the code to support dependency injection. Consumers initialize a config object, and then pass it to a config function. The config function can set internal variables or properties, and can be refactored steadily as your library begins supporting dependency injection.
Benefits:
- Gives you the benefits of both options 1 and 2.
- Allows you to progressively refactor your library code to support dependency injection if this is desirable.
Drawbacks:
- Same drawbacks as option 2.
The benefits of decoupling your library from the source of configuration are definitely worth any refactoring or redesigning that might be needed. Which option you choose depends on how tolerant consumers are for breaking changes. As with any breaking change, communicate early, and communicate often.