7

I have a class like this:

from __future__ import annotations

import os
from dataclasses import dataclass


@dataclass(frozen=True)
class Config:
    name: str
    age: str

    @staticmethod
    def init() -> Config:
        return Config(
            name=...
            age=...
        )

I would like to ensure that the init method always returns the same instance of Config.

I could achieve this by doing something like this:

@dataclass(frozen=True)
class Config:
    name: str
    age: str

    @staticmethod
    def init() -> Config:
        if not _private_instance:
            global _private_instance = Config(
                name=...
                age=...
            )
        return _private_instance

_private_instance: Optional[Config] = None

But I am wondering if there is a more Pythonic way of doing this.  Thanks
sixtyfootersdude
  • 25,859
  • 43
  • 145
  • 213
  • Does this answer your question? [Creating a singleton in Python](https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python) – quamrana May 11 '21 at 19:54
  • There's nothing wrong with your approach. I would say, the singleton pattern like this is not without it's detractors... and it's not something that is very common in Python – juanpa.arrivillaga May 11 '21 at 20:09
  • @quamrana - Thanks for the link. I read through that answer before posting. That question doesn't really work for my question. I am primarily interested in how to do this while using a `@dataclass`. – sixtyfootersdude May 11 '21 at 20:55

1 Answers1

2

The most pythonic way I found to achieve singleton dataclass is to use a singleton metaclass as suggested in the answer of the comment by @quamrana, with the addition of default values in order to prevent typechecker to raise warnings when you call the dataclass without arguments later.

from dataclasses import dataclass


class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


@dataclass(frozen=True)
class Config(metaclass=Singleton):
    name: str = ''
    age: str = ''


p1 = Config('John', '25')
print(p1)  # Config(name='John', age='25')
p2 = Config()
print(p2)  # Config(name='John', age='25')

print(p1 == p2)  # True
print(p1 is p2)  # True

The only issue with this code is that you are not forced to fill all arguments of the dataclass. If this is something you want to enforce with typechecker, remove the default values, but be aware that it will raise warning if you don't provide them later. Or, you can check if a proper value is set in the dunder post_init method:

@dataclass(frozen=True)
class Config(metaclass=Singleton):
    name: str = ''
    age: str = ''
    
    def __post_init__(self):
        if self.name == '' or self.age == '':
            raise ValueError('name or age is empty')

But this will be raised only at runtime.

I see in your question global variables. You should avoid using it. Example (this is not a good way of doing singleton dataclass, only fix your example without using global):

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional


@dataclass(frozen=True)
class Config:
    name: str
    age: str
    _private_instance: Optional[Config] = field(init=False, repr=False)

    @classmethod
    def init(cls) -> Config:
        if not cls._private_instance:
            cls._private_instance = Config(
                name=...,
                age=...,
            )
            return cls._private_instance

Also: your dataclass is used for configuration, maybe you should consider the library OmegaConf.

Dorian Turba
  • 3,260
  • 3
  • 23
  • 67
  • raise error, type object 'Config' has no attribute '_private_instance' in line `if not cls._private_instance`, in python3.11 – Panic Jun 30 '23 at 02:17
  • could you view this question, https://stackoverflow.com/questions/76585659/how-to-elegant-use-python-dataclass-singleton-for-config ,thanks – Panic Jun 30 '23 at 02:31