Easily manage settings with Pydantic
Posted on Jul 1, 2020 in Useful Python Modules , Python Programming - Advanced Level by Amo Chen ‐ 5 min read
During development, configuration files are often needed to easily change the behavior of the system. For example, the switch of Debug mode, database related settings, etc. are usually placed in configuration files.
In addition to the built-in module configparser provided by Python to easily implement configuration files, it can also easily implement configuration files using classes, such as the following two files are examples of configuration
# settings.py
class Settings(object):
DB_HOST = 'localhost'
DB_PORT = 8888
settings = Settings()
# test.py
from settings import settings
print(settings.DB_HOST, settings.DB_PORT)
However, nowadays development projects also often use dotenv
(e.g. python-dotenv) to make configuration easier.
In addition to developing the functionality of dotenv by yourself, you can actually choose to integrate class-style configuration files and dotenv easily with pydantic.
Requirements
- Python 3.7
- pydantic 1.5.1
$ pip install pydantic==1.5.1
pydantic
pydantic is a package that provides data type validation and configuration management through type annotations.
For example, the following example defines the Car class with pydantic’s BaseModel, and also annotates the types of each attribute in the class:
from datetime import datetime
from pydantic import BaseModel
class Car(BaseModel):
brand_id: int
brand_name: str
wheels: int = 4
created_at: datetime = datetime.now()
When we try to create an instance of the class, pydantic will perform type validation.
Car(brand_id='bad_id', brand_name='Bad name')
The above execution results will show errors similar to the following, indicating that we should set brand_id
to an integer type:
Traceback (most recent call last):
File "test.py", line 13, in <module>
Car(brand_id='bad_id', brand_name='Bad name')
File "pydantic/main.py", line 338, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Car
brand_id
value is not a valid integer (type=type_error.integer)
After getting an idea of what pydantic looks like, you can then try out the configuration management capabilities provided by pydantic.
Managing Configuration with Pydantic
The configuration management of pydantic can be implemented by inheriting the BaseSettings
class, which is the biggest difference from the BaseModel
class in that it provides integration with environment variables and dotenv
.
When we attempt to create an instance of the BaseSettings
class, BaseSettings
will attempt to load environment variables.
from pydantic import BaseSettings
class Settings(BaseSettings):
HOME: str
print(Settings())
The result of the above example is as follows, we can see that we haven’t set a value to HOME, and pydantic will automatically load the HOME in the environment variable to the HOME in Settings:
HOME='/Users/<username>'
Once we know the purpose of BaseSettings, we can design different settings for different environments, such as in the following example, we put the common settings in the Settings class and override the settings that are different due to the environment by inheriting the Settings class.
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
API_BASE: str
DB_HOST: str = 'localhost'
DB_PORT: int = 8888
DB_NAME: str
TESTING: bool = False
class Production(Settings):
API_BASE = 'https://example.com/api'
DB_NAME = 'production'
class Testing(Settings):
API_BASE = 'https://testing.example.com/api'
DB_NAME = 'testing'
TESTING = True
def get_settings():
env = os.getenv('ENV', 'TESTING')
if env == 'PRODUCTION':
return Production()
return Testing()
settings = get_settings()
print('DB Host =', settings.DB_HOST)
print('DB Port =', settings.DB_PORT)
print('Override =', Production(TESTING=True).dict())
The above example execution result as follow:
DB Host = localhost
DB Port = 8888
Override = {'API_BASE': 'https://example.com/api', 'DB_HOST': 'localhost', 'DB_PORT': 8888, 'DB_NAME': 'production', 'TESTING': True}
Let’s take a look at the data validation function provided by pydantic, when we deliberately set DB_PORT as a string:
Production(DB_PORT='abc')
The following error will appear, which indicates that DB_PORT must be an integer. This type of validation can also prevent problems with future configuration files due to mismatched types:
Traceback (most recent call last):
File "pydantic_test.py", line 40, in <module>
Production(DB_PORT='abc')
File "pydantic/env_settings.py", line 28, in pydantic.env_settings.BaseSettings.__init__
File "pydantic/main.py", line 338, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Production
DB_PORT
value is not a valid integer (type=type_error.integer)
Finally, take a look at the dotenv integration feature provided by pydantic.
In the BaseSettings class, dotenv can be easily integrated by defining the Config class and specifying the env_file property, for example:
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
API_BASE: str
DB_HOST: str
DB_PORT: int
DB_NAME: str
TESTING: bool = False
class Config:
env_file = '.env'
class Production(Settings):
API_BASE = 'https://example.com/api'
DB_NAME = 'production'
class Testing(Settings):
API_BASE = 'https://testing.example.com/api'
DB_NAME = 'testing'
class Config:
env_file = '.testing.env'
def get_settings():
env = os.getenv('ENV', 'TESTING')
if env == 'PRODUCTION':
return Production()
return Testing()
settings = get_settings()
print('DB Host =', settings.DB_HOST)
print('DB Port =', settings.DB_PORT)
print('Override =', Production(TESTING=True).dict())
The 13th, 14th, 26th and 27th lines in the above example are the parts for loading different dotenv configurations for different environments.
The contents and location of the two files, .env
and .testing.env
, in the file system are as follows:
.
├── .env
├── .testing.env
└── settings.py
The contents of .env:
DB_HOST=localhost
DB_PORT=8888
BLAH_BLAH=Banana
The content of testing.env:
DB_HOST=127.0.0.1
DB_PORT=6666
The result after integrating dotenv is:
DB Host = localhost
DB Port = 8888
Override = {'API_BASE': 'https://example.com/api', 'DB_HOST': 'localhost', 'DB_PORT': 8888, 'DB_NAME': 'production', 'TESTING': True}
It should be noted that BLAH_BLAH=Banana
in .env
is not one of the attributes defined in the abovementioned two categories of Settings
and Production
, so it will not be automatically loaded into Production
. If you want to load BLAH_BLAH, you must define the BLAH_BLAH property in the Settings or Production category.
Above is a simple introduction to configuration management using pydantic.
In fact, pydantic has other different uses, such as generating JSON schema, integrating Python 3.7 dataclasses, etc. If you are interested, you can read the official documents in detail, which is sure to bring a lot of benefits.