Dynamically configure Python Apps - FastAPI

Dynamically configure Python Apps - FastAPI
via Mika Baumesiter on Unsplash

Stop fumbling around with environment variables - this isn't 1997.


Like Dante’s inferno, every developer at some point has evolved their program through the many levels of configuration hell. Local variables, global variables, environment variables, environment variable files, variables pointing to different environment variable files. It can get...inconvenient.

Application configuration is more of an art than a science - it exists at the nexus of human and machine - the interface through which you get to tune your creation.

Let's not get too artsy about it though, ultimately we just want to not be constrained by our configuration method (*cough* environment variables *cough*), and we also want it to be easy and safe to modify our configuration during runtime.

There is a solution that meets these criteria- dynamically loaded JSON configuration.

Wow, so lucky, that’s exactly what we are building today. If you just want the sample code, check out the repo.


Who Should Read This?

Anyone who builds Python applications who wants a clean and convenient way to configure their application dynamically. Today we will be building a FastAPI web server, but similar techniques will apply to any Python application.

But what about DynaConf?

That is another way to accomplish our goal- although you will see in this implementation that it is hardly needed - the implementation complexity is minimal.

The FastAPI HelloWorld

So we start with our minimal application - nothing special here. If you are totally new to FastAPI and don't understand this code snippet, read this quickstart (only takes a couple minutes)

We run with uvicorn: “uvicorn main:app --port 8080”. Note that we are not specifying the “--reload” flag - this is a debug setting that reloads your application on changes by restarting it - but we want a more production ready setup that keeps the application running when reloading the config.

We can query our fledgling API like any other.

Choosing our Configuration Format

This is where you need to be a little bit opinionated. Maybe you like .yaml, maybe you like .toml, maybe you like wingdings inscribed into your hard-drive - but I prefer JSON. Lucky for you, you can easily apply the technique I will show you to the other data formats (mileage may vary with the wing dings).

We’ll just provide some basic configuration properties that any average application might require:

JSON is convenient because it allows you to represent arbitrarily complex data, and you get to leverage some great libraries. We will of course need to represent our applications configuration in our code, so for that we define a Config dataclass.

“@dataclass(frozen=True)” provides us with a nice immutable object (it provides no setter methods).

“@dataclass_json” provides very helpful “.to_json()” and “.from_json()” methods.

Look at how closely our Config class mimics our JSON configuration - awesome! And you can leverage the full power of Python dataclasses to validate values, make some fields optional, and much more.

Loading Config at Startup

This is quite simple - simply define a configuration file path (either statically in your application or via environment variable), and load it at application startup.


All we’re doing here is loading the file path to our config file from a good ole’ environment variable, and using that to read our file into our Config object using the aforementioned “.from_json()” helper method. Let’s give this a try:

Alright that’s great, but we only loaded our configuration at startup - which means changes made at runtime won’t do anything.

This is the part where most tutorials stop - but we’re going to keep going to achieve dynamic config loading.

Reloading Config During Runtime

Wouldn’t it be great if, without relaunching your application, you could modify your configuration and have the changes take effect in your code?

Well, it’s actually not that hard. There are several ways to solve this problem - the general idea being that you want a periodic background task that will reload your configuration. A couple quick safety concerns need to be addressed though:

  1. If the configuration is invalid, stick with the old one.
  2. Don’t allow “partial edits”. Imagine reloading the config half-way between “80” and “8080” - you might load “808” ! in your application!

The second point is typically solved by controlling how operators can modify configurations (copy the config, edit it, then 'mv' it to overwrite the existing config), so we won’t talk about it here.

Since we are using FastAPI, we can find a convenient utility that executes background tasks periodically, but if we were developing another type of application, we could accomplish something similar with celery or a background thread.

Our config reloading looks like this:

This will load our configuration at startup, and every 5 seconds afterwards. If we fail - we simply log an error and continue - keeping the old config "loaded" in our application.

Our app logs now demonstrate what is happening after startup:

Testing

Changing to an invalid configuration does not impact our runtime behavior:


On the other hand, valid configuration changes are automatically reloaded and we can see the changes in our application behavior:

Conclusion

That’s all for now! We now have a dynamically configured web application. To follow this up, we can generalize our background task so we can implement this solution for more than just a FastAPI web server - but that is out of scope for today.

To see the code used in this article, visit the Github repo.

References

Subscribe to VidaVolta

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe