grscheller.fp.singletons

Module fp.singletons - collection of singleton classes

Classes permitting at most only one instantiation. Safer, but not as performant, than a non-exported module level global. Difficult, but not impossible, for a typical end-user to exploit. Different versions tailored for different use cases.

Singleton types:

  • class NoValue: singleton instance representing the absence of a value
  • Class Sentinel: singleton instances used as a "hidden" sentinel value
  • class Nada: singleton instance representing & propagating failure
NoValue was designed as a None replacement

While None represents "returned no values," NoValue() represents the absence of a value. Non-existing values should not be comparable to anything, even themselves. End-users may use both None and () as sentinel values which colliding with using either to represent "nothingness."


Sentinel values used as hidden implementation details

Initially this one was somewhat close to mine and also enabled pickling. Subsequently it was "enhanced" to allow sentinel values to be subclassed. My implementation is substantially more simple, python implementation independent, less Gang-of-Four OOP, and more Pythonic.


Nada propagates failure

Nada is a singleton representing & propagating failure. Failure just blissfully propagates down "the happy path." For almost everything you do with it, it just returns itself. The maintainer has not used this construct enough yet to determine if it is a brilliant idea or a horrible blunder.

  1# Copyright 2023-2025 Geoffrey R. Scheller
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""### Module fp.singletons - collection of singleton classes
 16
 17Classes permitting at most only one instantiation. Safer, but not as performant,
 18than a non-exported module level global. Difficult, but not impossible, for
 19a typical end-user to exploit. Different versions tailored for different use
 20cases.
 21
 22#### Singleton types:
 23
 24* **class NoValue:** singleton instance representing the absence of a value
 25* **Class Sentinel:** singleton instances used as a "hidden" sentinel value
 26* **class Nada:** singleton instance representing & propagating failure
 27
 28##### `NoValue` was designed as a None replacement
 29
 30While `None` represents "returned no values," `NoValue()` represents the absence
 31of a value. Non-existing values should not be comparable to anything, even
 32themselves. End-users may use both `None` and `()` as sentinel values which
 33colliding with using either to represent "nothingness." 
 34
 35---
 36
 37##### `Sentinel` values used as hidden implementation details
 38
 39* Here is another implementation for Sentinel:``
 40  * on GitHub: [taleinat/python-stdlib-sentinels](https://github.com/taleinat/python-stdlib-sentinels)
 41  * on PyPI: [Project: Sentinels](https://pypi.org/project/sentinels/)
 42  * see: [PEP 661](https://peps.python.org/pep-0661/)
 43
 44Initially this one was somewhat close to mine and also enabled pickling.
 45Subsequently it was "enhanced" to allow sentinel values to be subclassed. My
 46implementation is substantially more simple, python implementation independent,
 47less Gang-of-Four OOP, and more Pythonic.
 48
 49---
 50
 51##### `Nada` propagates failure
 52
 53Nada is a singleton representing & propagating failure. Failure just blissfully
 54propagates down "the happy path." For almost everything you do with it, it just
 55returns itself. The maintainer has not used this construct enough yet to
 56determine if it is a brilliant idea or a horrible blunder.
 57"""
 58from __future__ import annotations
 59
 60__all__ = [ 'NoValue', 'Sentinel', 'Nada' ]
 61
 62from collections.abc import Callable, Iterator
 63from typing import Any, Final, final
 64
 65class NoValue():
 66    """Singleton class representing a missing value.
 67
 68    * similar to `None` but
 69      * while `None` represents "returned no values"
 70      * `NoValue()` represents the absence of a value
 71    * usage
 72      * `import NoValue from grscheller.fp.err_handling` and then
 73        * either use `NoValue()` directly
 74        * or define `_noValue: Final[NoValue] = NoValue()` don't export it
 75      * compare using `is` and `is not`
 76        * not `==` or `!=`
 77        * `None` means returned no values, so `None == None` makes sense
 78        * if one or both values are missing, then what is there to compare?
 79
 80    """
 81    __slots__ = ()
 82    _instance: NoValue|None = None
 83
 84    def __new__(cls) -> NoValue:
 85        if cls._instance is None:
 86            cls._instance = super(NoValue, cls).__new__(cls)
 87        return cls._instance
 88
 89    def __init__(self) -> None:
 90        return
 91
 92    def __repr__(self) -> str:
 93        return 'NoValue()'
 94
 95    def __eq__(self, other: object) -> bool:
 96        return False
 97
 98@final
 99class Sentinel():
100    """Singleton classes representing a sentinel values.
101
102    * intended for library code, not to be exported/shared between modules
103      * otherwise some of its intended typing guarantees may be lost
104    * useful substitute for `None` as a hidden sentinel value
105      * allows `None` to be stored in data structures
106      * allows end users to choose to use `None` or `()` as sentinel values
107      * always equals itself (unlike `noValue`)
108    * usage
109      * import Sentinel and then either
110        * define `_my_sentinel: Final[Sentinel] = Sentinel('my_sentinel')`
111        * or use `Sentinel('my_sentinel')` directly
112      * compare using either
113        * `is` and `is not` or `==` and `!=`
114        * the `Sentinel()` value always equals itself
115        * and never equals anything else, especially other sentinel values
116
117    """
118    __slots__ = '_sentinel_name',
119    _instances: dict[str, Sentinel] = {}
120
121    def __new__(cls, sentinel_name: str) -> Sentinel:
122        if sentinel_name not in cls._instances:
123            cls._instances[sentinel_name] = super(Sentinel, cls).__new__(cls)
124        return cls._instances[sentinel_name]
125
126    def __init__(self, sentinel_name: str) -> None:
127        self._sentinel_name = sentinel_name
128        return
129
130    def __repr__(self) -> str:
131        return "Sentinel('" + self._sentinel_name + "')"
132
133@final
134class Nada():
135    """Singleton class representing & propagating failure.
136
137    * singleton `_nada: nada = Nada()` represents a non-existent value
138    * returns itself for arbitrary method calls
139    * returns itself if called as a Callable with arbitrary arguments
140    * interpreted as an empty container by standard Python functions
141    * warning: non-standard equality semantics
142      * comparison compares true only when 2 non-missing values compare true
143      * thus `a == b` means two non-missing values compare as equal
144    * usage
145      * import `Nada` and then
146        * either use `Nada()` directly
147        * or define `_nada: Final[Nada] = Nada()` don't export it
148      * start propagating failure by setting a propagating value to Nada()
149        * works best when working with expression
150        * failure may fail to propagate
151          * for a function/method with just side effects
152          * engineer Nada() to fail to trigger side effects
153      * test for failure by comparing a result to `Nada()` itself using
154        * `is` and `is not`
155      * propagate failure through a calculation using
156        * `==` and `!=`
157        * the `Nada()` value never equals itself
158        * and never equals anything else
159
160    """
161    __slots__ = ()
162    _instance: Nada|None = None
163    _hash: int = 0
164
165    sentinel: Final[Sentinel] = Sentinel('Nada')
166
167    def __new__(cls) -> Nada:
168        if cls._instance is None:
169            cls._instance = super(Nada, cls).__new__(cls)
170            cls._hash = hash((cls._instance, (cls._instance,)))
171        return cls._instance
172
173    def __iter__(self) -> Iterator[Any]:
174        return iter(())
175
176    def __hash__(self) -> int:
177        return self._hash
178
179    def __repr__(self) -> str:
180        return 'Nada()'
181
182    def __bool__(self) -> bool:
183        return False
184
185    def __len__(self) -> int:
186        return 0
187
188    def __add__(self, right: Any) -> Nada:
189        return Nada()
190
191    def __radd__(self, left: Any) -> Nada:
192        return Nada()
193
194    def __mul__(self, right: Any) -> Nada:
195        return Nada()
196
197    def __rmul__(self, left: Any) -> Nada:
198        return Nada()
199
200    def __eq__(self, right: Any) -> bool:
201        return False
202
203    def __ne__(self, right: Any) -> bool:
204        return True
205
206    def __ge__(self, right: Any) -> bool:
207        return False
208
209    def __gt__(self, right: Any) -> bool:
210        return False
211
212    def __le__(self, right: Any) -> bool:
213        return False
214
215    def __lt__(self, right: Any) -> bool:
216        return False
217
218    def __getitem__(self, index: int|slice) -> Any:
219        return Nada()
220
221    def __setitem__(self, index: int|slice, item: Any) -> None:
222        return
223
224    def __call__(self, *args: Any, **kwargs: Any) -> Any:
225        return Nada()
226
227    def __getattr__(self, name: str) -> Callable[..., Any]:
228        def method(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any:
229            return Nada()
230        return method
231
232    def nada_get(self, alt: Any=sentinel) -> Any:
233        """Get an alternate value, defaults to `Nada()`."""
234        if alt == Sentinel('Nada'):
235            return Nada()
236        else:
237            return alt
class NoValue:
66class NoValue():
67    """Singleton class representing a missing value.
68
69    * similar to `None` but
70      * while `None` represents "returned no values"
71      * `NoValue()` represents the absence of a value
72    * usage
73      * `import NoValue from grscheller.fp.err_handling` and then
74        * either use `NoValue()` directly
75        * or define `_noValue: Final[NoValue] = NoValue()` don't export it
76      * compare using `is` and `is not`
77        * not `==` or `!=`
78        * `None` means returned no values, so `None == None` makes sense
79        * if one or both values are missing, then what is there to compare?
80
81    """
82    __slots__ = ()
83    _instance: NoValue|None = None
84
85    def __new__(cls) -> NoValue:
86        if cls._instance is None:
87            cls._instance = super(NoValue, cls).__new__(cls)
88        return cls._instance
89
90    def __init__(self) -> None:
91        return
92
93    def __repr__(self) -> str:
94        return 'NoValue()'
95
96    def __eq__(self, other: object) -> bool:
97        return False

Singleton class representing a missing value.

  • similar to None but
    • while None represents "returned no values"
    • NoValue() represents the absence of a value
  • usage
    • import NoValue from grscheller.fp.err_handling and then
      • either use NoValue() directly
      • or define _noValue: Final[NoValue] = NoValue() don't export it
    • compare using is and is not
      • not == or !=
      • None means returned no values, so None == None makes sense
      • if one or both values are missing, then what is there to compare?
@final
class Sentinel:
 99@final
100class Sentinel():
101    """Singleton classes representing a sentinel values.
102
103    * intended for library code, not to be exported/shared between modules
104      * otherwise some of its intended typing guarantees may be lost
105    * useful substitute for `None` as a hidden sentinel value
106      * allows `None` to be stored in data structures
107      * allows end users to choose to use `None` or `()` as sentinel values
108      * always equals itself (unlike `noValue`)
109    * usage
110      * import Sentinel and then either
111        * define `_my_sentinel: Final[Sentinel] = Sentinel('my_sentinel')`
112        * or use `Sentinel('my_sentinel')` directly
113      * compare using either
114        * `is` and `is not` or `==` and `!=`
115        * the `Sentinel()` value always equals itself
116        * and never equals anything else, especially other sentinel values
117
118    """
119    __slots__ = '_sentinel_name',
120    _instances: dict[str, Sentinel] = {}
121
122    def __new__(cls, sentinel_name: str) -> Sentinel:
123        if sentinel_name not in cls._instances:
124            cls._instances[sentinel_name] = super(Sentinel, cls).__new__(cls)
125        return cls._instances[sentinel_name]
126
127    def __init__(self, sentinel_name: str) -> None:
128        self._sentinel_name = sentinel_name
129        return
130
131    def __repr__(self) -> str:
132        return "Sentinel('" + self._sentinel_name + "')"

Singleton classes representing a sentinel values.

  • intended for library code, not to be exported/shared between modules
    • otherwise some of its intended typing guarantees may be lost
  • useful substitute for None as a hidden sentinel value
    • allows None to be stored in data structures
    • allows end users to choose to use None or () as sentinel values
    • always equals itself (unlike noValue)
  • usage
    • import Sentinel and then either
      • define _my_sentinel: Final[Sentinel] = Sentinel('my_sentinel')
      • or use Sentinel('my_sentinel') directly
    • compare using either
      • is and is not or == and !=
      • the Sentinel() value always equals itself
      • and never equals anything else, especially other sentinel values
Sentinel(sentinel_name: str)
127    def __init__(self, sentinel_name: str) -> None:
128        self._sentinel_name = sentinel_name
129        return
@final
class Nada:
134@final
135class Nada():
136    """Singleton class representing & propagating failure.
137
138    * singleton `_nada: nada = Nada()` represents a non-existent value
139    * returns itself for arbitrary method calls
140    * returns itself if called as a Callable with arbitrary arguments
141    * interpreted as an empty container by standard Python functions
142    * warning: non-standard equality semantics
143      * comparison compares true only when 2 non-missing values compare true
144      * thus `a == b` means two non-missing values compare as equal
145    * usage
146      * import `Nada` and then
147        * either use `Nada()` directly
148        * or define `_nada: Final[Nada] = Nada()` don't export it
149      * start propagating failure by setting a propagating value to Nada()
150        * works best when working with expression
151        * failure may fail to propagate
152          * for a function/method with just side effects
153          * engineer Nada() to fail to trigger side effects
154      * test for failure by comparing a result to `Nada()` itself using
155        * `is` and `is not`
156      * propagate failure through a calculation using
157        * `==` and `!=`
158        * the `Nada()` value never equals itself
159        * and never equals anything else
160
161    """
162    __slots__ = ()
163    _instance: Nada|None = None
164    _hash: int = 0
165
166    sentinel: Final[Sentinel] = Sentinel('Nada')
167
168    def __new__(cls) -> Nada:
169        if cls._instance is None:
170            cls._instance = super(Nada, cls).__new__(cls)
171            cls._hash = hash((cls._instance, (cls._instance,)))
172        return cls._instance
173
174    def __iter__(self) -> Iterator[Any]:
175        return iter(())
176
177    def __hash__(self) -> int:
178        return self._hash
179
180    def __repr__(self) -> str:
181        return 'Nada()'
182
183    def __bool__(self) -> bool:
184        return False
185
186    def __len__(self) -> int:
187        return 0
188
189    def __add__(self, right: Any) -> Nada:
190        return Nada()
191
192    def __radd__(self, left: Any) -> Nada:
193        return Nada()
194
195    def __mul__(self, right: Any) -> Nada:
196        return Nada()
197
198    def __rmul__(self, left: Any) -> Nada:
199        return Nada()
200
201    def __eq__(self, right: Any) -> bool:
202        return False
203
204    def __ne__(self, right: Any) -> bool:
205        return True
206
207    def __ge__(self, right: Any) -> bool:
208        return False
209
210    def __gt__(self, right: Any) -> bool:
211        return False
212
213    def __le__(self, right: Any) -> bool:
214        return False
215
216    def __lt__(self, right: Any) -> bool:
217        return False
218
219    def __getitem__(self, index: int|slice) -> Any:
220        return Nada()
221
222    def __setitem__(self, index: int|slice, item: Any) -> None:
223        return
224
225    def __call__(self, *args: Any, **kwargs: Any) -> Any:
226        return Nada()
227
228    def __getattr__(self, name: str) -> Callable[..., Any]:
229        def method(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any:
230            return Nada()
231        return method
232
233    def nada_get(self, alt: Any=sentinel) -> Any:
234        """Get an alternate value, defaults to `Nada()`."""
235        if alt == Sentinel('Nada'):
236            return Nada()
237        else:
238            return alt

Singleton class representing & propagating failure.

  • singleton _nada: nada = Nada() represents a non-existent value
  • returns itself for arbitrary method calls
  • returns itself if called as a Callable with arbitrary arguments
  • interpreted as an empty container by standard Python functions
  • warning: non-standard equality semantics
    • comparison compares true only when 2 non-missing values compare true
    • thus a == b means two non-missing values compare as equal
  • usage
    • import Nada and then
      • either use Nada() directly
      • or define _nada: Final[Nada] = Nada() don't export it
    • start propagating failure by setting a propagating value to Nada()
      • works best when working with expression
      • failure may fail to propagate
        • for a function/method with just side effects
        • engineer Nada() to fail to trigger side effects
    • test for failure by comparing a result to Nada() itself using
      • is and is not
    • propagate failure through a calculation using
      • == and !=
      • the Nada() value never equals itself
      • and never equals anything else
sentinel: Final[Sentinel] = Sentinel('Nada')
def nada_get(self, alt: Any = Sentinel('Nada')) -> Any:
233    def nada_get(self, alt: Any=sentinel) -> Any:
234        """Get an alternate value, defaults to `Nada()`."""
235        if alt == Sentinel('Nada'):
236            return Nada()
237        else:
238            return alt

Get an alternate value, defaults to Nada().