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.