Named Constructor

Named Constructor#

Twitter Handle LinkedIn Profile GitHub Profile Tag Code

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.

  1. 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.

  2. Alternative Constructors:

    • from_yaml: Creates an AppConfig instance from a YAML file.

    • from_json: Creates an AppConfig instance from a JSON file.

    • from_env: Creates an AppConfig instance from environment variables.

    • from_dict: A method that centralizes the logic for creating an AppConfig instance from a dictionary. This is used by all alternative constructors, promoting code reuse.

  3. 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:

  1. It provides a clean, consistent interface for creating AppConfig objects from various sources.

  2. It encapsulates the complexity of parsing different file formats and environment variables.

  3. 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')
)

References And Further Readings#