Named Constructor#
The from_xxx
pattern is increasingly popular in modern Python libraries. I did
not realise that it is actually called the Named Constructor
Pattern/Alternative Constructor Pattern. And it is also considered sometimes
an
anti-pattern.
Nevertheless, it is a pretty neat pattern and I use it a lot in my codebase.
Let’s consider a simple example below where we have a configuration management
system for a web application.
Multiple Configuration Classes:
DatabaseConfig
: Holds database-specific settings.CacheConfig
: Stores caching configuration.AppConfig
: The main configuration class that incorporates both database and cache configs, along with other application settings.
Alternative Constructors:
from_yaml
: Creates anAppConfig
instance from a YAML file.from_json
: Creates anAppConfig
instance from a JSON file.from_env
: Creates anAppConfig
instance from environment variables.from_dict
: A method that centralizes the logic for creating anAppConfig
instance from a dictionary. This is used by all alternative constructors, promoting code reuse.
Flexibility:
The system can handle different configuration sources (YAML, JSON, environment variables) without changing the core
AppConfig
structure.Additional settings can be included, allowing for extensibility.
This pattern is particularly useful in this scenario because:
It provides a clean, consistent interface for creating
AppConfig
objects from various sources.It encapsulates the complexity of parsing different file formats and environment variables.
It allows for easy extension to support additional configuration sources in the future.
1from __future__ import annotations
2
3import json
4import os
5import tempfile
6from typing import Any, Dict, Type
7
8import yaml
9from pydantic import BaseModel
10from rich.pretty import pprint
11
12
13class DatabaseConfig(BaseModel):
14 host: str
15 port: int
16 username: str
17 password: str
18 db_name: str
19
20
21class CacheConfig(BaseModel):
22 cache_type: str
23
24
25class AppConfig(BaseModel):
26 app_name: str
27 environment: str
28 debug: bool
29 database: DatabaseConfig
30 cache: CacheConfig
31
32 @classmethod
33 def from_yaml(cls: Type[AppConfig], file_path: str) -> AppConfig:
34 with open(file_path, "r") as file:
35 config_data = yaml.safe_load(file)
36 return cls.from_dict(config_data)
37
38 @classmethod
39 def from_json(cls: Type[AppConfig], file_path: str) -> AppConfig:
40 with open(file_path, "r") as file:
41 config_data = json.load(file)
42 return cls.from_dict(config_data)
43
44 @classmethod
45 def from_env(cls: Type[AppConfig]) -> AppConfig:
46 config_data = {
47 "app_name": os.getenv("APP_NAME"),
48 "environment": os.getenv("ENVIRONMENT"),
49 "debug": os.getenv("DEBUG", "false").lower() == "true",
50 "database": {
51 "host": os.getenv("DB_HOST"),
52 "port": int(os.getenv("DB_PORT", 5432)),
53 "username": os.getenv("DB_USERNAME"),
54 "password": os.getenv("DB_PASSWORD"),
55 "db_name": os.getenv("DB_NAME"),
56 },
57 "cache": {
58 "cache_type": os.getenv("CACHE_TYPE"),
59 "host": os.getenv("CACHE_HOST"),
60 "port": int(os.getenv("CACHE_PORT", 6379)),
61 },
62 }
63 return cls.from_dict(config_data)
64
65 @classmethod
66 def from_dict(cls: Type[AppConfig], config_data: Dict[str, Any]) -> AppConfig:
67 db_config = DatabaseConfig(**config_data["database"])
68 cache_config = CacheConfig(**config_data["cache"])
69 return cls(
70 app_name=config_data["app_name"],
71 environment=config_data["environment"],
72 debug=config_data["debug"],
73 database=db_config,
74 cache=cache_config,
75 )
76
77
78if __name__ == "__main__":
79 yaml_string = """
80 app_name: MyApp
81 environment: development
82 debug: true
83 database:
84 host: localhost
85 port: 5432
86 username: user
87 password: pass
88 db_name: mydb
89 cache:
90 cache_type: redis
91 host: localhost
92 port: 6379
93 """
94 with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".yaml") as temp_file:
95 temp_file.write(yaml_string)
96 temp_file_path = temp_file.name
97
98 try:
99 yaml_config = AppConfig.from_yaml(temp_file_path)
100 pprint(yaml_config)
101 finally:
102 os.unlink(temp_file_path)
103
104 # From JSON file
105 json_string = """
106 {
107 "app_name": "MyApp",
108 "environment": "development",
109 "debug": true,
110 "database": {
111 "host": "localhost",
112 "port": 5432,
113 "username": "user",
114 "password": "pass",
115 "db_name": "mydb"
116 },
117 "cache": {
118 "cache_type": "redis",
119 "host": "localhost",
120 "port": 6379
121 }
122 }
123 """
124 with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as temp_file:
125 temp_file.write(json_string)
126 temp_file_path = temp_file.name
127
128 try:
129 json_config = AppConfig.from_json(temp_file_path)
130 pprint(json_config)
131 finally:
132 os.unlink(temp_file_path)
AppConfig( │ app_name='MyApp', │ environment='development', │ debug=True, │ database=DatabaseConfig(host='localhost', port=5432, username='user', password='pass', db_name='mydb'), │ cache=CacheConfig(cache_type='redis') )
AppConfig( │ app_name='MyApp', │ environment='development', │ debug=True, │ database=DatabaseConfig(host='localhost', port=5432, username='user', password='pass', db_name='mydb'), │ cache=CacheConfig(cache_type='redis') )