10.5. Consider Module-Scoped Code to Configure Deployment Environments¶
A deployment environment is a configuration in which a program runs. Every program has at least one deployment environment: the production environment. The goal of writing a program in the first place is to put it to work in the production environment and achieve some kind of outcome.
Writing or modifying a program requires being able to run it on the computer you use for developing. The configuration of your development environment may be very different from that of your production environment. For example, you may be using a tiny single-board computer to develop a program that’s meant to run on enormous supercomputers.
Tools like venv (see Item 83: “Use Virtual Environments for Isolated and Reproducible Dependencies”) make it easy to ensure that all environments have the same Python packages installed. The trouble is that production environments often require many external assumptions that are hard to reproduce in development environments.
For example, say that I want to run a program in a web server container and give it access to a database. Every time I want to modify my program’s code, I need to run a server container, the database schema must be set up properly, and my program needs the password for access. This is a very high cost if all I’m trying to do is verify that a one-line change to my program works correctly.
The best way to work around such issues is to override parts of a program at startup time to provide different functionality depending on the deployment environment. For example, I could have two different main files—one for production and one for development:
# dev_main.py TESTING = True
import db_connection
db = db_connection.Database()
# prod_main.py TESTING = False
import db_connection
db = db_connection.Database()
The only difference between the two files is the value of the TESTING constant. Other modules in my program can then import the main module and use the value of TESTING to decide how they define their own attributes:
>>> # db_connection.py
>>> import __main__
>>>
>>> class TestingDatabase:
>>> ...
>>>
>>> class RealDatabase:
>>> ...
>>>
>>> if __main__.TESTING:
>>> Database = TestingDatabase
>>> else:
>>> Database = RealDatabase
The key behavior to notice here is that code running in module scope—not inside a function or method—is just normal Python code. You can use an if statement at the module level to decide how the module will define names. This makes it easy to tailor modules to your various deployment environments. You can avoid having to reproduce costly assumptions like database configurations when they aren’t needed. You can inject local or fake implementations that ease interactive development, or you can use mocks for writing tests (see Item 78: “Use Mocks to Test Code with Complex Dependencies”).
10.5.1. Note¶
When your deployment environment configuration gets really complicated, you should consider moving it out of Python constants (like TESTING) and into dedicated configuration files. Tools like the configparser built-in module let you maintain production configurations separately from code, a distinction that’s crucial for collaborating with an operations team.
This approach can be used for more than working around external assumptions. For example, if I know that my program must work differently depending on its host platform, I can inspect the sys module before defining top-level constructs in a module:
>>> #db_connection.py
>>> import sys
>>>
>>> class Win32Database:
>>> ...
>>>
>>> class PosixDatabase:
>>> ...
>>>
>>> if sys.platform.startswith('win32'):
>>> Database = Win32Database
>>> else:
>>> Database = PosixDatabase
Similarly, I could use environment variables from os.environ to guide my module definitions.
10.5.2. Things to Remember¶
✦ Programs often need to run in multiple deployment environments that each have unique assumptions and configurations.
✦ You can tailor a module’s contents to different deployment environments by using normal Python statements in module scope.
✦ Module contents can be the product of any external condition, including host introspection through the sys and os modules.