From e6d58fd46af83d8a2c96e26c0061d52d95afcf3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Tue, 17 Mar 2026 13:37:43 +0300 Subject: [PATCH 1/6] Update README with corrected markdown formatting, improved wording, and consistent terminology throughout --- README.md | 51 ++++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ace81e4..1a9a78a 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None - [**The problem**](#the-problem) - [**Installation**](#installation) -- [**The second None**](#the-second-none) -- [**Your own None objects**](#your-own-none-objects) +- [**The second `None`**](#the-second-none) +- [**Your own `None` objects**](#your-own-none-objects) - [**Type hinting**](#type-hinting) - [**Analogues**](#analogues) - [**FAQ**](#faq) @@ -59,13 +59,13 @@ As you can see, `denial` provides primitives only for rare cases of complex form ## Installation -Install it: +Install [`denial`](https://pypi.org/project/denial/) with `pip`: ```bash pip install denial ``` -You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld). +You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try out this package and others without installing them. ## The second `None` @@ -85,7 +85,7 @@ print(InnerNone == False) #> False ``` -This object is also an instance of [`InnerNoneType`](#your-own-none-objects) class (an analog of [`NoneType`](https://docs.python.org/3/library/types.html#types.NoneType), however, is not inherited from this), which makes it possible to check through [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance): +This object is also an instance of [`InnerNoneType`](#your-own-none-objects) class (an analog of [`NoneType`](https://docs.python.org/3/library/types.html#types.NoneType), however, is not inherited from this), which means you can check it with [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance): ```python from denial import InnerNoneType @@ -101,7 +101,7 @@ print(bool(InnerNone)) #> False ``` -> ⓘ It is recommended to use the `InnerNone` object inside libraries where a value close to `None` is required, but meaning a situation where the value is not really set, rather than set as `None`. This object should be completely isolated from the user code space. None of the public methods of your library should return this object. +> ⓘ It is recommended to use the `InnerNone` object inside libraries where a value close to `None` is required, but to mean a situation where the value is not really set, rather than set as `None`. This object should be completely isolated from the user code space. None of the public methods of your library should return this object. ## Your own `None` objects @@ -150,7 +150,7 @@ print(InnerNoneType(123) == 123) > ⚠️ For most situations, I do not recommend passing arguments to the class constructor. This can lead to situations where two identifiers from different parts of your code accidentally end up being the same, which can result in errors that are difficult to catch. If you do not pass arguments, the uniqueness of each `InnerNoneType` object created is guaranteed. -All `InnerNoneType` objects have beautiful string mappings: +All `InnerNoneType` objects have nice string representations: ```python print(InnerNone) @@ -163,7 +163,7 @@ print(InnerNoneType(123)) #> InnerNoneType(123, auto=False) ``` -You can also add a documentation string to the object, it will also be displayed: +You can also add a documentation string to the object, it will also appear in the string representation: ```python print(InnerNoneType(doc='My doc string!')) @@ -181,16 +181,16 @@ Documentation strings are not taken into account when comparing `InnerNoneType` > *[Official typing documentation](https://typing.python.org/en/latest/spec/special-types.html#none)* -`None` is a special value for which Python type checkers make an exception, allowing it to be used as an annotation of its own type. Unfortunately, this behavior cannot be reproduced without changing the internal implementation of existing type checkers, which I would not expect until the [PEP](https://peps.python.org/pep-0661/) is adopted. However, there is one type checker that can work with objects from `denial`: [`simtypes`](https://github.com/mutating/simtypes). But this thing is very primitive and is only intended for runtime. +`None` is a special value for which Python type checkers make an exception, allowing it to be used as an annotation of its own type. Unfortunately, this behavior cannot be reproduced without changing the internal implementation of existing type checkers, which I would not expect until the [PEP](https://peps.python.org/pep-0661/) is adopted. However, there is one type checker that can work with objects from `denial`: [`simtypes`](https://github.com/mutating/simtypes). But this tool is very primitive and is only intended for runtime. -Therefore, it is suggested to use class `InnerNoneType` as a type annotation: +Therefore, it is recommended to use the `InnerNoneType` class as a type annotation: ```python def function(default: int | InnerNoneType): ... ``` -In case you need a universal annotation for `None` and [`InnerNoneType`](#your-own-none-objects) objects, use the `SentinelType` annotation: +In case you need a universal annotation for `None` and [`InnerNoneType`](#your-own-none-objects) instances, use the `SentinelType` annotation: ```python from denial import SentinelType @@ -200,7 +200,7 @@ variable: SentinelType = InnerNoneType() variable: SentinelType = None # All 3 annotations are correct. ``` -And on the contrary, some programmers are very attentive to type safety and prefer to shift more of the work of checking types to automatic type checkers such as [`mypy`](https://mypy-lang.org/). In such cases, it may be useful to create your own types based on `InnerNoneType`: +And on the contrary, some programmers are very attentive to type safety and prefer to delegate more type checking to static type checkers such as [`mypy`](https://mypy-lang.org/). In such cases, it may be useful to create your own types based on `InnerNoneType`: ```python class MySentinelType(InnerNoneType): @@ -229,12 +229,12 @@ To avoid misunderstandings, if you mark a class with the `singleton` flag, all i ## Analogues -[The problem of distinguishing types of uncertainty](#the-problem) is often faced by programmers and they solve it in a variety of ways. This problem concerns all programming languages, because it ultimately describes our *knowledge*, and the [questions of cognition](https://colinmcginn.net/truth-value-gaps-and-meaning/) are universal for everyone. And everyone (including me!) has [*their own opinions*](https://en.wikipedia.org/wiki/Not_invented_here) on how to solve this problem. +Programmers often face [the problem of distinguishing types of uncertainty](#the-problem) and they solve it in a variety of ways. This problem concerns all programming languages, because it ultimately describes our *knowledge*, and the [questions about knowledge](https://colinmcginn.net/truth-value-gaps-and-meaning/) are universal for everyone. And everyone (including me!) has [*their own opinions*](https://en.wikipedia.org/wiki/Not_invented_here) on how to solve this problem. ![standards](https://imgs.xkcd.com/comics/standards.png) > *Current state of affairs* -Some programming languages are a little better thought out in this matter than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. I think this is due to the fact that [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only 2 of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes the languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. +Some programming languages are a little better thought out in this matter than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. I think this is due to the fact that [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only 2 of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. The Python standard library uses at least [15 sentinel objects](https://mail.python.org/archives/list/python-dev@python.org/message/JBYXQH3NV3YBF7P2HLHB5CD6V3GVTY55/): @@ -254,11 +254,11 @@ The Python standard library uses at least [15 sentinel objects](https://mail.pyt - **sched: _sentinel** - **traceback: _sentinel** -Since the language itself does not regulate this in any way, there is chaos and code duplication. Before creating this library, I used one of them, but later realized that importing a module that I don't need for anything other than sentinel is a bad idea. +Since the language itself does not regulate this in any way, there is chaos and code duplication. Before creating this library, I used one of them, but later realized that importing a module that I don't need for anything other than its sentinel object is a bad idea. Not only did I come to this conclusion, the community also tried to standardize it. A standard for sentinels was proposed in [PEP-661](https://peps.python.org/pep-0661/), but at the time of writing it has still not been adopted, as there is no consensus on a number of important issues. This topic was also indirectly raised in [PEP-484](https://peps.python.org/pep-0484/), as well as in [PEP-695](https://peps.python.org/pep-0695/) and in [PEP-696](https://peps.python.org/pep-0696/). Unfortunately, while there is no "official" solution, everyone is still forced to reinvent the wheel on their own. Some, such as [Pydantic](https://github.com/pydantic/pydantic/issues/12090), are proactive, as if `PEP-661` has already been adopted. Personally, I don't like the solution proposed in `PEP-661`, mainly because of the implementation examples that suggest using a global registry of all created sentinels, which can lead to memory leaks and concurrency limitations. -In addition to `denial`, there are many packages with sentinels in [`Pypi`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed to me overcomplicated for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and contains some other code defects. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of autogenerating sentinel ids there. Of course, there are other packages that I haven't reviewed here. +In addition to `denial`, there are many packages with sentinels in [`PyPI`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed to me overcomplicated for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and contains some other code defects. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of auto-generating sentinel IDs there. Of course, there are other packages that I haven't reviewed here. And of course, there are still different ways to implement primitive sentinels in your code in a few lines of code without using third-party packages. @@ -271,7 +271,12 @@ A: Sentinel seems like a very simple task conceptually, we just need more `None` Q2: Why is the uniqueness of the values not ensured? The `None` object is a singleton. In Python, it is impossible to access the `None` name and get a different value. But in `denial`, it is possible for a user to create two different objects by passing two identical IDs there. In rare cases, this can lead to unintended errors, for example, if the same identifier is accidentally used in two different places in the program. Why is that? -A: To ensure that a certain value is used in the program only once, there are 2 possible ways: 1. create a registry of all such values and check each new value for uniqueness in runtime; 2. check the source code statically, for example using a special [linter](https://en.wikipedia.org/wiki/Lint_(software)). I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. Would you say that no one will create them in a loop? Well, I'm not ready to take any chances. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to the [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the percentage of sequential execution time in the program, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). Because I can imagine situations where creating sentinels would be a fairly frequent operation and it would create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). Current compromise: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), which then checks the uniqueness of the object. +A: To ensure that a certain value is used in the program only once, there are 2 possible ways: + +1. create a registry of all such values and check each new value for uniqueness at runtime. +2. check the source code statically, for example using a special [linter](https://en.wikipedia.org/wiki/Lint_(software)). + +I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. Would you say that no one will create them in a loop? Well, I'm not ready to take any chances. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to the [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the percentage of sequential execution time in the program, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). Because I can imagine situations where creating sentinels would be a fairly frequent operation and it would create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). Current compromise: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), which then checks the uniqueness of the object. Q3: What could be the reasons to use `InnerNoneType` with arguments? It always seems like a bad idea. How about removing this feature altogether? @@ -279,28 +284,28 @@ A: This is *almost always* a bad idea. But in some extremely *rare cases*, it ca Q4: Why not use a separate class with singleton objects for each situation when we need a sentinel? Then it will be possible to make checks through [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance), and it will also be possible to write more accurate type hints. -A: The ability to use classes as type hints is a compelling argument. It would be possible to create several classes in different parts of the program, assigning different semantics to each of them, and then checking compliance using a type checker such as [`mypy`](https://mypy-lang.org/). However, I did not make this a basic mechanism for `denial`, as I believe that in most cases the semantics will not actually differ. At the same time, creating a new class each time is more verbose than creating objects. However, I left the option to inherit from `InnerNoneType` if you still consider it necessary in your code. Objects of inheriting classes (if you do not override the behavior of the class in any way) will behave the same as `InnerNoneType` objects. But they will not be singletons, which allows you to group several different objects with the same semantics within a single class. +A: The ability to use classes as type hints is a compelling argument. It would be possible to create several classes in different parts of the program, assigning different semantics to each of them, and then checking compliance using a type checker such as [`mypy`](https://mypy-lang.org/). However, I did not make this a basic mechanism for `denial`, as I believe that in most cases the semantics will not actually differ. At the same time, creating a new class each time is more verbose than creating objects. However, I left the option to inherit from `InnerNoneType` if you still consider it necessary in your code. Instances of derived classes (if you do not override the behavior of the class in any way) will behave the same as `InnerNoneType` objects. But they will not be singletons, which allows you to group several different objects with the same semantics within a single class. -Q5: You're using only one `InnerNoneType` class, but the internal id that makes objects unique can be either generated automatically or passed by the user. Doesn't this mean that it would be worthwhile to allocate 2 independent classes? +Q5: You're using only one `InnerNoneType` class, but the internal ID that makes objects unique can be either generated automatically or passed by the user. Doesn't this mean that it would be worthwhile to split this into two separate classes? A: I did this to reduce cognitive load. I haven't seen any cases where a clear division into two classes provides a practical (rather than aesthetic) benefit, while you don't have to think about which class to import and how its use differs. Q6: Why is `InnerNoneType` not inherited from `NoneType`? -A: The purpose of these classes is really quite similar. However, I felt that inheriting from `NoneType` could lead to breakdowns in the old code, which might expect that only one instance of `NoneType` is possible, and therefore uses the `isinstance` check as an analogue of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. Perhaps you should collect such statistics using the GitHub API. +A: The purpose of these classes is really quite similar. However, I felt that inheriting from `NoneType` could break existing code, which might expect that only one instance of `NoneType` is possible, and therefore uses the `isinstance` check as an analogue of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. Perhaps you should collect such statistics using the GitHub API. Q7: How is the uniqueness of `InnerNoneType` objects ensured? -A: If you create `InnerNoneType` objects without passing any arguments to the constructor, an id that is unique within the process is created inside each object when it is created. It is by this id that the object will check whether it is equal to another `InnerNoneType` object. It will be equal to another object only if it has the same id inside it, which is usually impossible, and therefore the object remains equal only to itself. If you passed your own id when creating the object, the automatic id is not created, yours is used. In this case, it is your job to track possible unwanted intersections. The library can also distinguish between objects where the id is created automatically and where it is passed from outside, using a special flag inside each value. This guarantees that there are no intersections between automatically generated and non-automatically generated ids. +A: If you create `InnerNoneType` objects without passing any arguments to the constructor, an ID that is unique within the process is created inside each object when it is created. It is by this ID that the object will check whether it is equal to another `InnerNoneType` object. It will be equal to another object only if it has the same ID inside it, which is usually impossible, and therefore the object remains equal only to itself. If you passed your own ID when creating the object, the automatic ID is not created, yours is used. In this case, it is your job to track possible collisions. The library can also distinguish between objects where the ID is created automatically and where it is passed from outside, using a special flag inside each value. This guarantees that there are no intersections between automatically generated and non-automatically generated IDs. Q8: Why all these complications and an additional library for sentinels? I just write `sentinel = object()` in my code and then do checks like `x is sentinel`. It works, but you've overcomplicated things. -A: Indeed, we already have one source of unique ids for objects: their addresses in memory. Checks like `x is sentinel` can be identical in meaning to those used in this library. However, this option has two significant drawbacks. First, you lose the compactness of string representation that `denial` provides. Second, this method does not allow you to create two identical sentinel objects if you want to, which prevents you from, for example, transferring sentinel objects over the network or between processes. Unfortunately, this is impossible with memory addresses. Since this library is positioned as universal, I had to abandon this option. +A: Indeed, we already have one source of unique IDs for objects: their addresses in memory. Checks like `x is sentinel` can be identical in meaning to those used in this library. However, this option has two significant drawbacks. First, you lose the compactness of string representation that `denial` provides. Second, this method does not allow you to create two identical sentinel objects if you want to, which prevents you from, for example, transferring sentinel objects over the network or between processes. Unfortunately, this is impossible with memory addresses. Since this library is positioned as universal, I had to abandon this option. Q9: Why don't we use [Enums](https://docs.python.org/3/library/enum.html) as sentinels? It's already in the standard library, no need to invent anything. And it can do all the things we expect from sentinels. A: [Various](https://t.me/ru_python/2680685) [people](https://www.reddit.com/r/Python/comments/1qraodv/comment/o2n0d94/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) have suggested this method to me, and it was also mentioned in [PEP-661](https://peps.python.org/pep-0661/). The PEP argues that Enum's `__repr__` is too long. In `denial`, I made a short and informative `__repr__`, which should be sufficient in principle. However, here are some other reasons not to use Enum: 1. Denial can be used in recursive algorithms where the number of nesting levels is unknown in advance. This is possible because the number of variables with sentinels is not limited here. In the case of Enum, you must know the number of nesting levels in advance. 2. Using a single Enum class with all sentinels in the program contradicts the idea of modularity. In essence, this is equivalent to using global variables, which usually indicates code with a “bad smell.” If you create an Enum class for each sentinel, it will look too verbose. 3. Under the hood, Enum uses the global registry approach that I discussed in the FAQ section of the README. 4. Enum usually has slightly different semantics. For example, I can hardly imagine a situation where I would want to iterate through all sentinels. 5. Enum is generally [too complex](https://t.me/opensource_findings/883) a tool for such a simple purpose. In my opinion, the entire Enum module should have been deprecated long ago. The sheer size of the module's documentation and the existence of several official manuals for it suggest that something is wrong here. 6. Another argument may seem ridiculous given the slowness of CPython, but I'll mention it anyway. Enum forces the user to use values via a dot, such as `EnumClass.SENTINEL`. This leads to an unnecessary lookup operation and a slight slowdown of the program. However, I haven't done any measurements, so perhaps this has been optimized in some way by the CPython developers. -Q10: Why does the `singleton` flag prohibit the creation of a second class object if you can simply return the same object? +Q10: Why does the `singleton` flag prohibit the creation of a second instance if you can simply return the same object? A: There are different possible implementations of the [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern). In Python code, you often see implementations that “hide” the uniqueness check of an instance under the hood and simply return the first instance when you try to create a second one. But in my opinion, this implementation of the pattern is flawed because it hides the true nature of objects from the reader, which violates [the Zen of Python](https://en.wikipedia.org/wiki/Zen_of_Python) ("explicit is better than implicit"). The code implicitly propagates attempts to create multiple objects of the same class, when in fact only one object is needed. In my opinion, if we want an object to have only one instance, we should explicitly prohibit the creation of more than one instance. In this case, there will be no more than one instance of the class in the code, which makes the code base more consistent and transparent. From 8e5e96418be144cc923ea5ea73e6a8169440df71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Tue, 17 Mar 2026 13:54:02 +0300 Subject: [PATCH 2/6] Fix typos and improve wording throughout README.md --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1a9a78a..cf24100 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ![logo](https://raw.githubusercontent.com/mutating/denial/develop/docs/assets/logo_1.svg) -Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None) constant may not be sufficient to [distinguish situations](https://en.wikipedia.org/wiki/Semipredicate_problem) where a value is *undefined* from situations where it is *defined as undefined*. Does that sound too abstract? Then read below the more detailed [description of the problem](#the-problem) and what [solutions](#analogues) exist for it. +Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None) constant may not be sufficient to [distinguish situations](https://en.wikipedia.org/wiki/Semipredicate_problem) where a value is *undefined* from situations where it is *defined as undefined*. Does that sound too abstract? Then read the more detailed [description of the problem](#the-problem) and its [solutions](#analogues) below. ## Table of contents @@ -34,9 +34,9 @@ Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None ## The problem -Programmers encounter uncertainty everywhere. We [don't know](https://en.wikipedia.org/wiki/Semipredicate_problem) in advance whether a user will enter a valid value into a form, or whether a given operation on two numbers is possible. To highlight uncertainty as a separate entity, programmers have come up with so-called [sentinel objects](https://en.wikipedia.org/wiki/Sentinel_value). These can be very different: [NULL](https://en.wikipedia.org/wiki/Null_pointer), [`None`](https://docs.python.org/3/library/constants.html#None), [nil](https://ru.wikipedia.org/wiki/Nil), [undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined), [NaN](https://en.wikipedia.org/wiki/NaN), and an infinite number of others. +Programmers encounter uncertainty everywhere. We [don't know](https://en.wikipedia.org/wiki/Semipredicate_problem) in advance whether a user will enter a valid value into a form, or whether a given operation on two numbers is possible. To highlight uncertainty as a separate entity, programmers have come up with so-called [sentinel objects](https://en.wikipedia.org/wiki/Sentinel_value). These can take many forms: [NULL](https://en.wikipedia.org/wiki/Null_pointer), [`None`](https://docs.python.org/3/library/constants.html#None), [nil](https://ru.wikipedia.org/wiki/Nil), [undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined), [NaN](https://en.wikipedia.org/wiki/NaN), and an infinite number of others. -Different programming languages and environments offer [different models](#analogues) for representing uncertainty as objects. This is usually related to how a particular language has evolved and what forms of uncertainty its users most often encounter. Globally, I distinguish [three](https://numberwarrior.wordpress.com/2010/07/30/is-one-two-many-a-myth/) main models: +Different programming languages and environments offer [different models](#analogues) for representing uncertainty as objects. This is usually related to how a particular language has evolved and what forms of uncertainty its users most often encounter. Broadly, I distinguish [three](https://numberwarrior.wordpress.com/2010/07/30/is-one-two-many-a-myth/) main models: - **One simple sentinel object**. This approach works great in most cases. In most real code, we don't need to distinguish between more than one type of uncertainty. This is the default model offered by Python (although there is much room for debate here: for example, [exceptions](https://docs.python.org/3/tutorial/errors.html#exceptions) can, in a sense, also be considered sentinel objects). However, it breaks down when we need to [distinguish between](https://en.wikipedia.org/wiki/I_know_that_I_know_nothing) situations where *we know we don't know* something and situations where *we don't know that we don't know* something. @@ -48,7 +48,7 @@ Different programming languages and environments offer [different models](#analo > *Yes, this library was also created by [primitive cultures](https://en.wiktionary.org/wiki/Pythonist#English)* -The first option is almost always sufficient. The `denial` library offers special primitives that cover the second and third options, providing complete coverage of uncertainty options for Python: +The first option is almost always sufficient. The `denial` library offers special primitives that cover the second and third options, covering the remaining uncertainty cases in Python: - The first option is built into Python and does not require any third-party libraries: [`None`](https://docs.python.org/3/library/constants.html#None). - The second option is represented by the [`InnerNone`](#the-second-none) constant from `denial`. It is practically the same as `None`, just a second `None`. @@ -70,7 +70,7 @@ You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try ## The second `None` -This library defines an object that is proposed to be used in almost the same way as a regular `None`. This is how it is imported: +This library defines an object intended to be used in almost the same way as a regular `None`. This is how it is imported: ```python from denial import InnerNone @@ -85,7 +85,7 @@ print(InnerNone == False) #> False ``` -This object is also an instance of [`InnerNoneType`](#your-own-none-objects) class (an analog of [`NoneType`](https://docs.python.org/3/library/types.html#types.NoneType), however, is not inherited from this), which means you can check it with [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance): +This object is also an instance of the [`InnerNoneType`](#your-own-none-objects) class (an analog of [`NoneType`](https://docs.python.org/3/library/types.html#types.NoneType), though it does not inherit from it), which means you can check it with [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance): ```python from denial import InnerNoneType @@ -94,14 +94,14 @@ print(isinstance(InnerNone, InnerNoneType)) #> True ``` -Like `None`, `InnerNone` (as well as all other `InnerNoneType` objects) always returns `False` when cast to `bool`: +Like `None`, `InnerNone` (as well as all other `InnerNoneType` objects) always converts to `False` with `bool()`: ```python print(bool(InnerNone)) #> False ``` -> ⓘ It is recommended to use the `InnerNone` object inside libraries where a value close to `None` is required, but to mean a situation where the value is not really set, rather than set as `None`. This object should be completely isolated from the user code space. None of the public methods of your library should return this object. +> ⓘ It is recommended to use the `InnerNone` object inside libraries where a value close to `None` is required, but to indicate that a value is unset rather than explicitly set to `None`. This object should be kept entirely out of user-facing code. None of the public methods of your library should return this object. ## Your own `None` objects @@ -163,7 +163,7 @@ print(InnerNoneType(123)) #> InnerNoneType(123, auto=False) ``` -You can also add a documentation string to the object, it will also appear in the string representation: +You can also add a documentation string to the object. It will also appear in the string representation: ```python print(InnerNoneType(doc='My doc string!')) @@ -181,7 +181,7 @@ Documentation strings are not taken into account when comparing `InnerNoneType` > *[Official typing documentation](https://typing.python.org/en/latest/spec/special-types.html#none)* -`None` is a special value for which Python type checkers make an exception, allowing it to be used as an annotation of its own type. Unfortunately, this behavior cannot be reproduced without changing the internal implementation of existing type checkers, which I would not expect until the [PEP](https://peps.python.org/pep-0661/) is adopted. However, there is one type checker that can work with objects from `denial`: [`simtypes`](https://github.com/mutating/simtypes). But this tool is very primitive and is only intended for runtime. +`None` is a special value for which Python type checkers make an exception, allowing it to be used as an annotation of its own type. Unfortunately, this behavior cannot be reproduced without changing the internal implementation of existing type checkers, which I would not expect to happen unless the [PEP](https://peps.python.org/pep-0661/) is adopted. However, there is one type checker that can work with objects from `denial`: [`simtypes`](https://github.com/mutating/simtypes). But this tool is very primitive and is only intended for runtime. Therefore, it is recommended to use the `InnerNoneType` class as a type annotation: @@ -200,7 +200,7 @@ variable: SentinelType = InnerNoneType() variable: SentinelType = None # All 3 annotations are correct. ``` -And on the contrary, some programmers are very attentive to type safety and prefer to delegate more type checking to static type checkers such as [`mypy`](https://mypy-lang.org/). In such cases, it may be useful to create your own types based on `InnerNoneType`: +On the other hand, some programmers are very attentive to type safety and prefer to delegate more type checking to static type checkers such as [`mypy`](https://mypy-lang.org/). In such cases, it may be useful to create your own types based on `InnerNoneType`: ```python class MySentinelType(InnerNoneType): @@ -224,12 +224,12 @@ second_sentinel = MySentinelType() #> denial.errors.DoubleSingletonsInstantiationError: Class "MySentinelType" is marked with a flag prohibiting the creation of more than one instance. ``` -To avoid misunderstandings, if you mark a class with the `singleton` flag, all its descendants must also have this tag. +To avoid misunderstandings, if you mark a class with the `singleton` flag, all its descendants must also have this flag. ## Analogues -Programmers often face [the problem of distinguishing types of uncertainty](#the-problem) and they solve it in a variety of ways. This problem concerns all programming languages, because it ultimately describes our *knowledge*, and the [questions about knowledge](https://colinmcginn.net/truth-value-gaps-and-meaning/) are universal for everyone. And everyone (including me!) has [*their own opinions*](https://en.wikipedia.org/wiki/Not_invented_here) on how to solve this problem. +Programmers often face [the problem of distinguishing types of uncertainty](#the-problem) and they solve it in a variety of ways. This problem concerns all programming languages, because it ultimately describes our *knowledge*, and the [questions about knowledge](https://colinmcginn.net/truth-value-gaps-and-meaning/) are universal. And everyone (including me!) has [*their own opinions*](https://en.wikipedia.org/wiki/Not_invented_here) on how to solve this problem. ![standards](https://imgs.xkcd.com/comics/standards.png) > *Current state of affairs* @@ -256,9 +256,9 @@ The Python standard library uses at least [15 sentinel objects](https://mail.pyt Since the language itself does not regulate this in any way, there is chaos and code duplication. Before creating this library, I used one of them, but later realized that importing a module that I don't need for anything other than its sentinel object is a bad idea. -Not only did I come to this conclusion, the community also tried to standardize it. A standard for sentinels was proposed in [PEP-661](https://peps.python.org/pep-0661/), but at the time of writing it has still not been adopted, as there is no consensus on a number of important issues. This topic was also indirectly raised in [PEP-484](https://peps.python.org/pep-0484/), as well as in [PEP-695](https://peps.python.org/pep-0695/) and in [PEP-696](https://peps.python.org/pep-0696/). Unfortunately, while there is no "official" solution, everyone is still forced to reinvent the wheel on their own. Some, such as [Pydantic](https://github.com/pydantic/pydantic/issues/12090), are proactive, as if `PEP-661` has already been adopted. Personally, I don't like the solution proposed in `PEP-661`, mainly because of the implementation examples that suggest using a global registry of all created sentinels, which can lead to memory leaks and concurrency limitations. +I wasn't the only one to come to this conclusion; the community also tried to standardize it. A standard for sentinels was proposed in [PEP-661](https://peps.python.org/pep-0661/), but at the time of writing it has still not been adopted, as there is no consensus on a number of important issues. This topic was also indirectly raised in [PEP-484](https://peps.python.org/pep-0484/), as well as in [PEP-695](https://peps.python.org/pep-0695/) and in [PEP-696](https://peps.python.org/pep-0696/). Unfortunately, while there is no "official" solution, everyone is still forced to reinvent the wheel on their own. Some, such as [Pydantic](https://github.com/pydantic/pydantic/issues/12090), are proactive, as if `PEP-661` has already been adopted. Personally, I don't like the solution proposed in `PEP-661`, mainly because of the implementation examples that suggest using a global registry of all created sentinels, which can lead to memory leaks and concurrency limitations. -In addition to `denial`, there are many packages with sentinels in [`PyPI`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed to me overcomplicated for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and contains some other code defects. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of auto-generating sentinel IDs there. Of course, there are other packages that I haven't reviewed here. +In addition to `denial`, there are many packages with sentinels in [`PyPI`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed overcomplicated to me for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and has some other implementation issues. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of auto-generating sentinel IDs there. Of course, there are other packages that I haven't reviewed here. And of course, there are still different ways to implement primitive sentinels in your code in a few lines of code without using third-party packages. @@ -267,24 +267,24 @@ And of course, there are still different ways to implement primitive sentinels i Q1: Is this library the best option for sentinels? -A: Sentinel seems like a very simple task conceptually, we just need more `None`'s. But suddenly, creating a good sentinel option is one of the most difficult issues. There are too many ways to do this and too many trade-offs in which you need to choose a side. The design of sentinel objects is similar to the creation of axioms: it delves deep into parts of our psyche that are not usually subject to critical analysis, and therefore it is very difficult to talk about the problems that arise. So I'm not claiming to be the best solution to this issue, but I've tried to eliminate all the obvious disadvantages that don't involve trading. I'm not sure if it's even possible to find *the best solution* in this area, so all I can do is make *[an arbitrary decision](https://en.wikipedia.org/wiki/Analysis_paralysis)* and stick to it. If you want, join me. +A: Sentinel seems like a very simple task conceptually, we just need more `None` values. In practice, creating a good sentinel design is one of the most difficult issues. There are too many ways to do this and too many trade-offs in which you need to choose a side. The design of sentinel objects is similar to the creation of axioms: it delves deep into parts of our psyche that are not usually subject to critical analysis, and therefore it is very difficult to talk about the problems that arise. So I'm not claiming to be the best solution to this issue, but I've tried to eliminate all the obvious disadvantages that don't involve trade-offs. I'm not sure if it's even possible to find *the best solution* in this area, so all I can do is make *[an arbitrary decision](https://en.wikipedia.org/wiki/Analysis_paralysis)* and stick to it. If you want, join me. Q2: Why is the uniqueness of the values not ensured? The `None` object is a singleton. In Python, it is impossible to access the `None` name and get a different value. But in `denial`, it is possible for a user to create two different objects by passing two identical IDs there. In rare cases, this can lead to unintended errors, for example, if the same identifier is accidentally used in two different places in the program. Why is that? -A: To ensure that a certain value is used in the program only once, there are 2 possible ways: +A: To ensure that a certain value is used in the program only once, there are two possible ways: 1. create a registry of all such values and check each new value for uniqueness at runtime. 2. check the source code statically, for example using a special [linter](https://en.wikipedia.org/wiki/Lint_(software)). -I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. Would you say that no one will create them in a loop? Well, I'm not ready to take any chances. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to the [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the percentage of sequential execution time in the program, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). Because I can imagine situations where creating sentinels would be a fairly frequent operation and it would create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). Current compromise: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), which then checks the uniqueness of the object. +I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. Would you say that no one will create them in a loop? Well, I'm not ready to take any chances. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to a [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the percentage of sequential execution time in the program, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). Because I can imagine situations where creating sentinels would be a fairly frequent operation and it would create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). The current compromise is this: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), and that value becomes the object's unique identifier. Q3: What could be the reasons to use `InnerNoneType` with arguments? It always seems like a bad idea. How about removing this feature altogether? -A: This is *almost always* a bad idea. But in some extremely *rare cases*, it can be useful. It may be that two sections of code that do not know about each other will want to transfer a compatible sentinel to each other. It is even possible that it will be transmitted over the network and "recreated" on the other side. It is for such cases that the option to use your own identifiers has been left. But it's better to use empty brackets. +A: This is *almost always* a bad idea. But in some extremely *rare cases*, it can be useful. It may be that two sections of code that do not know about each other may need to exchange a compatible sentinel. It is even possible that it will be transmitted over the network and "recreated" on the other side. It is for such cases that the option to use your own identifiers has been left. But it's better to call `InnerNoneType()` without arguments. Q4: Why not use a separate class with singleton objects for each situation when we need a sentinel? Then it will be possible to make checks through [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance), and it will also be possible to write more accurate type hints. -A: The ability to use classes as type hints is a compelling argument. It would be possible to create several classes in different parts of the program, assigning different semantics to each of them, and then checking compliance using a type checker such as [`mypy`](https://mypy-lang.org/). However, I did not make this a basic mechanism for `denial`, as I believe that in most cases the semantics will not actually differ. At the same time, creating a new class each time is more verbose than creating objects. However, I left the option to inherit from `InnerNoneType` if you still consider it necessary in your code. Instances of derived classes (if you do not override the behavior of the class in any way) will behave the same as `InnerNoneType` objects. But they will not be singletons, which allows you to group several different objects with the same semantics within a single class. +A: The ability to use classes as type hints is a compelling argument. It would be possible to create several classes in different parts of the program, assigning different semantics to each of them, and then checking compatibility using a type checker such as [`mypy`](https://mypy-lang.org/). However, I did not make this a basic mechanism for `denial`, as I believe that in most cases the semantics will not actually differ. At the same time, creating a new class each time is more verbose than creating objects. However, I left the option to inherit from `InnerNoneType` if you still consider it necessary in your code. Instances of derived classes (if you do not override the behavior of the class in any way) will behave the same as `InnerNoneType` objects. But they will not be singletons, which allows you to group several different objects with the same semantics within a single class. Q5: You're using only one `InnerNoneType` class, but the internal ID that makes objects unique can be either generated automatically or passed by the user. Doesn't this mean that it would be worthwhile to split this into two separate classes? From 6536d10ff3ae43f3901468d9096674fee3803ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Tue, 17 Mar 2026 13:54:10 +0300 Subject: [PATCH 3/6] Bump version to 0.0.13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e959ec8..63e6956 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "denial" -version = "0.0.12" +version = "0.0.13" authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] description = "Is one None not enough for you? There's more" readme = "README.md" From 78d1a45e6f11e658d2f132017ddabb7602d53338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Tue, 17 Mar 2026 14:16:15 +0300 Subject: [PATCH 4/6] Fix typos and update references to "Analogs" from "Analogues" throughout documentation --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cf24100..143b94f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ![logo](https://raw.githubusercontent.com/mutating/denial/develop/docs/assets/logo_1.svg) -Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None) constant may not be sufficient to [distinguish situations](https://en.wikipedia.org/wiki/Semipredicate_problem) where a value is *undefined* from situations where it is *defined as undefined*. Does that sound too abstract? Then read the more detailed [description of the problem](#the-problem) and its [solutions](#analogues) below. +Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None) constant may not be sufficient to [distinguish situations](https://en.wikipedia.org/wiki/Semipredicate_problem) where a value is *undefined* from situations where it is *defined as undefined*. If that sounds abstract, see the detailed [description of the problem](#the-problem) and its [solutions](#analogs) below. ## Table of contents @@ -28,7 +28,7 @@ Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None - [**The second `None`**](#the-second-none) - [**Your own `None` objects**](#your-own-none-objects) - [**Type hinting**](#type-hinting) -- [**Analogues**](#analogues) +- [**Analogs**](#analogs) - [**FAQ**](#faq) @@ -36,7 +36,7 @@ Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None Programmers encounter uncertainty everywhere. We [don't know](https://en.wikipedia.org/wiki/Semipredicate_problem) in advance whether a user will enter a valid value into a form, or whether a given operation on two numbers is possible. To highlight uncertainty as a separate entity, programmers have come up with so-called [sentinel objects](https://en.wikipedia.org/wiki/Sentinel_value). These can take many forms: [NULL](https://en.wikipedia.org/wiki/Null_pointer), [`None`](https://docs.python.org/3/library/constants.html#None), [nil](https://ru.wikipedia.org/wiki/Nil), [undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined), [NaN](https://en.wikipedia.org/wiki/NaN), and an infinite number of others. -Different programming languages and environments offer [different models](#analogues) for representing uncertainty as objects. This is usually related to how a particular language has evolved and what forms of uncertainty its users most often encounter. Broadly, I distinguish [three](https://numberwarrior.wordpress.com/2010/07/30/is-one-two-many-a-myth/) main models: +Different programming languages and environments offer [different models](#analogs) for representing uncertainty as objects. This is usually related to how a particular language has evolved and what forms of uncertainty its users most often encounter. Broadly, I distinguish [three](https://numberwarrior.wordpress.com/2010/07/30/is-one-two-many-a-myth/) main models: - **One simple sentinel object**. This approach works great in most cases. In most real code, we don't need to distinguish between more than one type of uncertainty. This is the default model offered by Python (although there is much room for debate here: for example, [exceptions](https://docs.python.org/3/tutorial/errors.html#exceptions) can, in a sense, also be considered sentinel objects). However, it breaks down when we need to [distinguish between](https://en.wikipedia.org/wiki/I_know_that_I_know_nothing) situations where *we know we don't know* something and situations where *we don't know that we don't know* something. @@ -70,13 +70,13 @@ You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try ## The second `None` -This library defines an object intended to be used in almost the same way as a regular `None`. This is how it is imported: +This library defines an object intended to be used in almost the same way as the built-in `None`. This is how it is imported: ```python from denial import InnerNone ``` -This object is equal only to itself: +Like `None`, this object compares equal only to itself: ```python print(InnerNone == InnerNone) @@ -150,7 +150,7 @@ print(InnerNoneType(123) == 123) > ⚠️ For most situations, I do not recommend passing arguments to the class constructor. This can lead to situations where two identifiers from different parts of your code accidentally end up being the same, which can result in errors that are difficult to catch. If you do not pass arguments, the uniqueness of each `InnerNoneType` object created is guaranteed. -All `InnerNoneType` objects have nice string representations: +All `InnerNoneType` objects have concise string representations: ```python print(InnerNone) @@ -181,7 +181,7 @@ Documentation strings are not taken into account when comparing `InnerNoneType` > *[Official typing documentation](https://typing.python.org/en/latest/spec/special-types.html#none)* -`None` is a special value for which Python type checkers make an exception, allowing it to be used as an annotation of its own type. Unfortunately, this behavior cannot be reproduced without changing the internal implementation of existing type checkers, which I would not expect to happen unless the [PEP](https://peps.python.org/pep-0661/) is adopted. However, there is one type checker that can work with objects from `denial`: [`simtypes`](https://github.com/mutating/simtypes). But this tool is very primitive and is only intended for runtime. +`None` is a special value for which Python type checkers make an exception, allowing it to be used as an annotation of its own type. Unfortunately, this behavior cannot be reproduced without changing the internal implementation of existing type checkers, which I would not expect to happen unless the [PEP](https://peps.python.org/pep-0661/) is adopted. However, there is one type checker that can work with objects from `denial`: [`simtypes`](https://github.com/mutating/simtypes). But this tool is minimal and intended only for runtime use. Therefore, it is recommended to use the `InnerNoneType` class as a type annotation: @@ -200,7 +200,7 @@ variable: SentinelType = InnerNoneType() variable: SentinelType = None # All 3 annotations are correct. ``` -On the other hand, some programmers are very attentive to type safety and prefer to delegate more type checking to static type checkers such as [`mypy`](https://mypy-lang.org/). In such cases, it may be useful to create your own types based on `InnerNoneType`: +On the other hand, some programmers care deeply about type safety and prefer to delegate more type checking to static type checkers such as [`mypy`](https://mypy-lang.org/). In such cases, it may be useful to create your own types based on `InnerNoneType`: ```python class MySentinelType(InnerNoneType): @@ -210,7 +210,7 @@ def some_function(sentinel: MySentinelType): ... ``` -Using the derived class at runtime is completely identical to using the original `InnerNoneType` type and allows you to conveniently narrow down the scope of use of a specific type. +At runtime, a derived class behaves exactly like `InnerNoneType` and allows you to conveniently narrow down the scope of use of a specific type. If you need to prevent more than one instance of your class from being created, use the `singleton` flag: @@ -227,14 +227,14 @@ second_sentinel = MySentinelType() To avoid misunderstandings, if you mark a class with the `singleton` flag, all its descendants must also have this flag. -## Analogues +## Analogs Programmers often face [the problem of distinguishing types of uncertainty](#the-problem) and they solve it in a variety of ways. This problem concerns all programming languages, because it ultimately describes our *knowledge*, and the [questions about knowledge](https://colinmcginn.net/truth-value-gaps-and-meaning/) are universal. And everyone (including me!) has [*their own opinions*](https://en.wikipedia.org/wiki/Not_invented_here) on how to solve this problem. ![standards](https://imgs.xkcd.com/comics/standards.png) > *Current state of affairs* -Some programming languages are a little better thought out in this matter than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. I think this is due to the fact that [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only 2 of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. +Some languages provide more explicit built-in support for this distinction than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. I think this is due to the fact that [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only 2 of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. The Python standard library uses at least [15 sentinel objects](https://mail.python.org/archives/list/python-dev@python.org/message/JBYXQH3NV3YBF7P2HLHB5CD6V3GVTY55/): @@ -254,9 +254,9 @@ The Python standard library uses at least [15 sentinel objects](https://mail.pyt - **sched: _sentinel** - **traceback: _sentinel** -Since the language itself does not regulate this in any way, there is chaos and code duplication. Before creating this library, I used one of them, but later realized that importing a module that I don't need for anything other than its sentinel object is a bad idea. +Because Python does not standardize sentinels, projects often implement them independently. Before creating this library, I used one of them, but later realized that importing a module that I don't need for anything other than its sentinel object is a bad idea. -I wasn't the only one to come to this conclusion; the community also tried to standardize it. A standard for sentinels was proposed in [PEP-661](https://peps.python.org/pep-0661/), but at the time of writing it has still not been adopted, as there is no consensus on a number of important issues. This topic was also indirectly raised in [PEP-484](https://peps.python.org/pep-0484/), as well as in [PEP-695](https://peps.python.org/pep-0695/) and in [PEP-696](https://peps.python.org/pep-0696/). Unfortunately, while there is no "official" solution, everyone is still forced to reinvent the wheel on their own. Some, such as [Pydantic](https://github.com/pydantic/pydantic/issues/12090), are proactive, as if `PEP-661` has already been adopted. Personally, I don't like the solution proposed in `PEP-661`, mainly because of the implementation examples that suggest using a global registry of all created sentinels, which can lead to memory leaks and concurrency limitations. +I wasn't the only one to come to this conclusion; the community also tried to standardize it. A standard for sentinels was proposed in [PEP-661](https://peps.python.org/pep-0661/), but at the time of writing it has still not been adopted, as there is no consensus on a number of important issues. This topic was also indirectly raised in [PEP-484](https://peps.python.org/pep-0484/), as well as in [PEP-695](https://peps.python.org/pep-0695/) and in [PEP-696](https://peps.python.org/pep-0696/). Without an official solution, projects continue to reinvent the wheel. Some projects, such as [Pydantic](https://github.com/pydantic/pydantic/issues/12090), already behave as though `PEP-661` were adopted. Personally, I don't like the solution proposed in `PEP-661`, mainly because of the implementation examples that suggest using a global registry of all created sentinels, which can lead to memory leaks and concurrency limitations. In addition to `denial`, there are many packages with sentinels in [`PyPI`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed overcomplicated to me for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and has some other implementation issues. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of auto-generating sentinel IDs there. Of course, there are other packages that I haven't reviewed here. @@ -267,7 +267,7 @@ And of course, there are still different ways to implement primitive sentinels i Q1: Is this library the best option for sentinels? -A: Sentinel seems like a very simple task conceptually, we just need more `None` values. In practice, creating a good sentinel design is one of the most difficult issues. There are too many ways to do this and too many trade-offs in which you need to choose a side. The design of sentinel objects is similar to the creation of axioms: it delves deep into parts of our psyche that are not usually subject to critical analysis, and therefore it is very difficult to talk about the problems that arise. So I'm not claiming to be the best solution to this issue, but I've tried to eliminate all the obvious disadvantages that don't involve trade-offs. I'm not sure if it's even possible to find *the best solution* in this area, so all I can do is make *[an arbitrary decision](https://en.wikipedia.org/wiki/Analysis_paralysis)* and stick to it. If you want, join me. +A: Sentinels seem conceptually simple: we just need more `None`-like values. In practice, creating a good sentinel design is one of the most difficult issues. There are too many ways to do this and too many trade-offs in which you need to choose a side. The design of sentinel objects is similar to the creation of axioms: it delves deep into parts of our psyche that are not usually subject to critical analysis, and therefore it is very difficult to talk about the problems that arise. So I'm not claiming to be the best solution to this issue, but I've tried to eliminate all the obvious disadvantages that don't involve trade-offs. I'm not sure if it's even possible to find *the best solution* in this area, so all I can do is make *[an arbitrary decision](https://en.wikipedia.org/wiki/Analysis_paralysis)* and stick to it. If you want, join me. Q2: Why is the uniqueness of the values not ensured? The `None` object is a singleton. In Python, it is impossible to access the `None` name and get a different value. But in `denial`, it is possible for a user to create two different objects by passing two identical IDs there. In rare cases, this can lead to unintended errors, for example, if the same identifier is accidentally used in two different places in the program. Why is that? @@ -276,7 +276,7 @@ A: To ensure that a certain value is used in the program only once, there are tw 1. create a registry of all such values and check each new value for uniqueness at runtime. 2. check the source code statically, for example using a special [linter](https://en.wikipedia.org/wiki/Lint_(software)). -I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. Would you say that no one will create them in a loop? Well, I'm not ready to take any chances. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to a [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the percentage of sequential execution time in the program, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). Because I can imagine situations where creating sentinels would be a fairly frequent operation and it would create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). The current compromise is this: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), and that value becomes the object's unique identifier. +I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. Would you say that no one will create them in a loop? Well, I'm not ready to take any chances. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to a [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the percentage of sequential execution time in the program, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). This matters because creating sentinels may be frequent in some workloads, which could create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). The current compromise is this: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), and that value becomes the object's unique identifier. Q3: What could be the reasons to use `InnerNoneType` with arguments? It always seems like a bad idea. How about removing this feature altogether? @@ -292,7 +292,7 @@ A: I did this to reduce cognitive load. I haven't seen any cases where a clear d Q6: Why is `InnerNoneType` not inherited from `NoneType`? -A: The purpose of these classes is really quite similar. However, I felt that inheriting from `NoneType` could break existing code, which might expect that only one instance of `NoneType` is possible, and therefore uses the `isinstance` check as an analogue of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. Perhaps you should collect such statistics using the GitHub API. +A: The purpose of these classes is really quite similar. However, I felt that inheriting from `NoneType` could break existing code, which might expect that only one instance of `NoneType` is possible, and therefore uses the `isinstance` check as an analog of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. It would be interesting to measure this using the GitHub API. Q7: How is the uniqueness of `InnerNoneType` objects ensured? @@ -304,7 +304,7 @@ A: Indeed, we already have one source of unique IDs for objects: their addresses Q9: Why don't we use [Enums](https://docs.python.org/3/library/enum.html) as sentinels? It's already in the standard library, no need to invent anything. And it can do all the things we expect from sentinels. -A: [Various](https://t.me/ru_python/2680685) [people](https://www.reddit.com/r/Python/comments/1qraodv/comment/o2n0d94/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) have suggested this method to me, and it was also mentioned in [PEP-661](https://peps.python.org/pep-0661/). The PEP argues that Enum's `__repr__` is too long. In `denial`, I made a short and informative `__repr__`, which should be sufficient in principle. However, here are some other reasons not to use Enum: 1. Denial can be used in recursive algorithms where the number of nesting levels is unknown in advance. This is possible because the number of variables with sentinels is not limited here. In the case of Enum, you must know the number of nesting levels in advance. 2. Using a single Enum class with all sentinels in the program contradicts the idea of modularity. In essence, this is equivalent to using global variables, which usually indicates code with a “bad smell.” If you create an Enum class for each sentinel, it will look too verbose. 3. Under the hood, Enum uses the global registry approach that I discussed in the FAQ section of the README. 4. Enum usually has slightly different semantics. For example, I can hardly imagine a situation where I would want to iterate through all sentinels. 5. Enum is generally [too complex](https://t.me/opensource_findings/883) a tool for such a simple purpose. In my opinion, the entire Enum module should have been deprecated long ago. The sheer size of the module's documentation and the existence of several official manuals for it suggest that something is wrong here. 6. Another argument may seem ridiculous given the slowness of CPython, but I'll mention it anyway. Enum forces the user to use values via a dot, such as `EnumClass.SENTINEL`. This leads to an unnecessary lookup operation and a slight slowdown of the program. However, I haven't done any measurements, so perhaps this has been optimized in some way by the CPython developers. +A: [Various](https://t.me/ru_python/2680685) [people](https://www.reddit.com/r/Python/comments/1qraodv/comment/o2n0d94/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) have suggested this method to me, and it was also mentioned in [PEP-661](https://peps.python.org/pep-0661/). The PEP argues that Enum's `__repr__` is too long. In `denial`, I made a short and informative `__repr__`, which should be sufficient in principle. However, here are some other reasons not to use Enum: 1. Denial can be used in recursive algorithms where the number of nesting levels is unknown in advance. This is possible because the number of variables with sentinels is not limited here. In the case of Enum, you must know the number of nesting levels in advance. 2. Using a single Enum class with all sentinels in the program contradicts the idea of modularity. In essence, this is equivalent to using global variables, which is usually a code smell. If you create an Enum class for each sentinel, it will look too verbose. 3. Under the hood, Enum uses the global registry approach that I discussed in the FAQ section of the README. 4. Enum usually has slightly different semantics. For example, I can hardly imagine a situation where I would want to iterate through all sentinels. 5. Enum is generally [too complex](https://t.me/opensource_findings/883) a tool for such a simple purpose. In my opinion, the entire Enum module should have been deprecated long ago. The sheer size of the module's documentation and the existence of several official manuals for it suggest that something is wrong here. 6. Another argument may seem ridiculous given the slowness of CPython, but I'll mention it anyway. Enum forces the user to use values via a dot, such as `EnumClass.SENTINEL`. This leads to an unnecessary lookup operation and a slight slowdown of the program. However, I haven't done any measurements, so perhaps this has been optimized in some way by the CPython developers. Q10: Why does the `singleton` flag prohibit the creation of a second instance if you can simply return the same object? From f65656fdbcda1f820061ec8f833e70323ef17e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Tue, 17 Mar 2026 14:35:12 +0300 Subject: [PATCH 5/6] Fix typos and improve clarity in README.md --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 143b94f..f7d87c3 100644 --- a/README.md +++ b/README.md @@ -36,19 +36,19 @@ Python's built-in [`None`](https://docs.python.org/3/library/constants.html#None Programmers encounter uncertainty everywhere. We [don't know](https://en.wikipedia.org/wiki/Semipredicate_problem) in advance whether a user will enter a valid value into a form, or whether a given operation on two numbers is possible. To highlight uncertainty as a separate entity, programmers have come up with so-called [sentinel objects](https://en.wikipedia.org/wiki/Sentinel_value). These can take many forms: [NULL](https://en.wikipedia.org/wiki/Null_pointer), [`None`](https://docs.python.org/3/library/constants.html#None), [nil](https://ru.wikipedia.org/wiki/Nil), [undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined), [NaN](https://en.wikipedia.org/wiki/NaN), and an infinite number of others. -Different programming languages and environments offer [different models](#analogs) for representing uncertainty as objects. This is usually related to how a particular language has evolved and what forms of uncertainty its users most often encounter. Broadly, I distinguish [three](https://numberwarrior.wordpress.com/2010/07/30/is-one-two-many-a-myth/) main models: +Different programming languages and environments offer [different models](#analogs) for representing uncertainty as objects. This is usually related to how a particular language has evolved and what forms of uncertainty its users most often encounter. Broadly, I see [three](https://numberwarrior.wordpress.com/2010/07/30/is-one-two-many-a-myth/) main models: - **One simple sentinel object**. This approach works great in most cases. In most real code, we don't need to distinguish between more than one type of uncertainty. This is the default model offered by Python (although there is much room for debate here: for example, [exceptions](https://docs.python.org/3/tutorial/errors.html#exceptions) can, in a sense, also be considered sentinel objects). However, it breaks down when we need to [distinguish between](https://en.wikipedia.org/wiki/I_know_that_I_know_nothing) situations where *we know we don't know* something and situations where *we don't know that we don't know* something. - **Two sentinel objects**. This is more common in languages where, for example, a lot of user input is processed and where it is necessary to distinguish between different types of empty values. If our task is to program Socrates, that will be quite sufficient. -- **An infinite recursive hierarchy of sentinel objects**. From a philosophical point of view, uncertainty cannot be considered as a finite object, because that would already be a definite judgment about uncertainty. Therefore, we should consider uncertainty as consisting of an infinite number of layers. In practice, such structures can arise, for example, when we extract data from a large number of diverse sources but want to clearly distinguish at which stage of the pipeline the data was not found. +- **An infinite recursive hierarchy of sentinel objects**. From a philosophical point of view, uncertainty cannot be considered a finite object, because that would already be a definite judgment about uncertainty. Therefore, we should consider uncertainty as consisting of an infinite number of layers. In practice, such structures can arise, for example, when we extract data from a large number of diverse sources but want to clearly distinguish at which stage of the pipeline the data was not found. ![One, Two, Many](https://imgs.xkcd.com/comics/one_two.png) > *Yes, this library was also created by [primitive cultures](https://en.wiktionary.org/wiki/Pythonist#English)* -The first option is almost always sufficient. The `denial` library offers special primitives that cover the second and third options, covering the remaining uncertainty cases in Python: +The first option is almost always sufficient. The `denial` library offers special primitives that cover the second and third options to cover the remaining uncertainty cases in Python: - The first option is built into Python and does not require any third-party libraries: [`None`](https://docs.python.org/3/library/constants.html#None). - The second option is represented by the [`InnerNone`](#the-second-none) constant from `denial`. It is practically the same as `None`, just a second `None`. @@ -70,7 +70,7 @@ You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try ## The second `None` -This library defines an object intended to be used in almost the same way as the built-in `None`. This is how it is imported: +This library defines an object that is used much like built-in `None`. This is how it is imported: ```python from denial import InnerNone @@ -94,7 +94,7 @@ print(isinstance(InnerNone, InnerNoneType)) #> True ``` -Like `None`, `InnerNone` (as well as all other `InnerNoneType` objects) always converts to `False` with `bool()`: +Like `None`, `InnerNone` (as well as all other `InnerNoneType` objects) is always falsy: ```python print(bool(InnerNone)) @@ -190,7 +190,7 @@ def function(default: int | InnerNoneType): ... ``` -In case you need a universal annotation for `None` and [`InnerNoneType`](#your-own-none-objects) instances, use the `SentinelType` annotation: +If you need a universal annotation for `None` and [`InnerNoneType`](#your-own-none-objects) instances, use the `SentinelType` annotation: ```python from denial import SentinelType @@ -210,7 +210,7 @@ def some_function(sentinel: MySentinelType): ... ``` -At runtime, a derived class behaves exactly like `InnerNoneType` and allows you to conveniently narrow down the scope of use of a specific type. +At runtime, a derived class behaves exactly like `InnerNoneType` and lets you narrow the intended usage of a specific sentinel type. If you need to prevent more than one instance of your class from being created, use the `singleton` flag: @@ -234,7 +234,7 @@ Programmers often face [the problem of distinguishing types of uncertainty](#the ![standards](https://imgs.xkcd.com/comics/standards.png) > *Current state of affairs* -Some languages provide more explicit built-in support for this distinction than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. I think this is due to the fact that [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only 2 of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. +Some languages provide more explicit built-in support for this distinction than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. Likely because to the fact that [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only two of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. The Python standard library uses at least [15 sentinel objects](https://mail.python.org/archives/list/python-dev@python.org/message/JBYXQH3NV3YBF7P2HLHB5CD6V3GVTY55/): @@ -258,7 +258,7 @@ Because Python does not standardize sentinels, projects often implement them ind I wasn't the only one to come to this conclusion; the community also tried to standardize it. A standard for sentinels was proposed in [PEP-661](https://peps.python.org/pep-0661/), but at the time of writing it has still not been adopted, as there is no consensus on a number of important issues. This topic was also indirectly raised in [PEP-484](https://peps.python.org/pep-0484/), as well as in [PEP-695](https://peps.python.org/pep-0695/) and in [PEP-696](https://peps.python.org/pep-0696/). Without an official solution, projects continue to reinvent the wheel. Some projects, such as [Pydantic](https://github.com/pydantic/pydantic/issues/12090), already behave as though `PEP-661` were adopted. Personally, I don't like the solution proposed in `PEP-661`, mainly because of the implementation examples that suggest using a global registry of all created sentinels, which can lead to memory leaks and concurrency limitations. -In addition to `denial`, there are many packages with sentinels in [`PyPI`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed overcomplicated to me for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and has some other implementation issues. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of auto-generating sentinel IDs there. Of course, there are other packages that I haven't reviewed here. +In addition to `denial`, there are many packages with sentinels on [`PyPI`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed overcomplicated to me for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and has some other implementation issues. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of auto-generating sentinel IDs there. Of course, there are other packages that I haven't reviewed here. And of course, there are still different ways to implement primitive sentinels in your code in a few lines of code without using third-party packages. @@ -267,7 +267,7 @@ And of course, there are still different ways to implement primitive sentinels i Q1: Is this library the best option for sentinels? -A: Sentinels seem conceptually simple: we just need more `None`-like values. In practice, creating a good sentinel design is one of the most difficult issues. There are too many ways to do this and too many trade-offs in which you need to choose a side. The design of sentinel objects is similar to the creation of axioms: it delves deep into parts of our psyche that are not usually subject to critical analysis, and therefore it is very difficult to talk about the problems that arise. So I'm not claiming to be the best solution to this issue, but I've tried to eliminate all the obvious disadvantages that don't involve trade-offs. I'm not sure if it's even possible to find *the best solution* in this area, so all I can do is make *[an arbitrary decision](https://en.wikipedia.org/wiki/Analysis_paralysis)* and stick to it. If you want, join me. +A: Sentinels seem conceptually simple: we just need more `None`-like values. In practice, creating a good sentinel design is one of the most difficult issues. There are too many ways to do this and too many trade-offs in which you need to choose a side. So I'm not claiming to be the best solution to this issue, but I've tried to eliminate all the obvious disadvantages that don't involve trade-offs. I'm not sure if it's even possible to find *the best solution* in this area, so all I can do is make *[an arbitrary decision](https://en.wikipedia.org/wiki/Analysis_paralysis)* and stick to it. If you want, join me. Q2: Why is the uniqueness of the values not ensured? The `None` object is a singleton. In Python, it is impossible to access the `None` name and get a different value. But in `denial`, it is possible for a user to create two different objects by passing two identical IDs there. In rare cases, this can lead to unintended errors, for example, if the same identifier is accidentally used in two different places in the program. Why is that? @@ -276,13 +276,13 @@ A: To ensure that a certain value is used in the program only once, there are tw 1. create a registry of all such values and check each new value for uniqueness at runtime. 2. check the source code statically, for example using a special [linter](https://en.wikipedia.org/wiki/Lint_(software)). -I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. Would you say that no one will create them in a loop? Well, I'm not ready to take any chances. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to a [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the percentage of sequential execution time in the program, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). This matters because creating sentinels may be frequent in some workloads, which could create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). The current compromise is this: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), and that value becomes the object's unique identifier. +I found the second option too difficult for now, so the first one remains. The main problem is the possibility of [memory leaks](https://en.wikipedia.org/wiki/Memory_leak). There is a good general rule for programming: rely as little as possible on global state, because it can create unexpected side effects. For example, if you create unique identifiers in a loop, the registry may overflow. You might argue that nobody would create them in a loop, but I’d rather not rely on that. It also creates problems with concurrency. The fact is that checking the value in the registry and entering it into the registry are two independent operations that take some time between them, which means that errors are possible due to a [race condition](https://en.wikipedia.org/wiki/Race_condition). If you protect this operation with a [mutex](https://en.wikipedia.org/wiki/Lock_(computer_science)), it will increase the sequential portion of execution time, which means it will slow down the entire program due to [Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law). This matters if sentinel creation is frequent in a given workload, which could create performance problems (it's time to make fun of Python's performance because of the [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock), but I hope for a better future). The current compromise is this: always use [`InnerNoneType`](#your-own-none-objects) without arguments, unless you have a serious reason to do otherwise. In this case, the uniqueness of each object is guaranteed, since "under the hood", each time a new object is created, an internal counter is incremented (thread-safe!), and that value becomes the object's unique identifier. -Q3: What could be the reasons to use `InnerNoneType` with arguments? It always seems like a bad idea. How about removing this feature altogether? +Q3: Why would you use `InnerNoneType` with arguments? It always seems like a bad idea. How about removing this feature altogether? A: This is *almost always* a bad idea. But in some extremely *rare cases*, it can be useful. It may be that two sections of code that do not know about each other may need to exchange a compatible sentinel. It is even possible that it will be transmitted over the network and "recreated" on the other side. It is for such cases that the option to use your own identifiers has been left. But it's better to call `InnerNoneType()` without arguments. -Q4: Why not use a separate class with singleton objects for each situation when we need a sentinel? Then it will be possible to make checks through [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance), and it will also be possible to write more accurate type hints. +Q4: Why not use a separate singleton class for each sentinel use case? Then it will be possible to make checks through [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance), and it will also be possible to write more accurate type hints. A: The ability to use classes as type hints is a compelling argument. It would be possible to create several classes in different parts of the program, assigning different semantics to each of them, and then checking compatibility using a type checker such as [`mypy`](https://mypy-lang.org/). However, I did not make this a basic mechanism for `denial`, as I believe that in most cases the semantics will not actually differ. At the same time, creating a new class each time is more verbose than creating objects. However, I left the option to inherit from `InnerNoneType` if you still consider it necessary in your code. Instances of derived classes (if you do not override the behavior of the class in any way) will behave the same as `InnerNoneType` objects. But they will not be singletons, which allows you to group several different objects with the same semantics within a single class. @@ -292,15 +292,15 @@ A: I did this to reduce cognitive load. I haven't seen any cases where a clear d Q6: Why is `InnerNoneType` not inherited from `NoneType`? -A: The purpose of these classes is really quite similar. However, I felt that inheriting from `NoneType` could break existing code, which might expect that only one instance of `NoneType` is possible, and therefore uses the `isinstance` check as an analog of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. It would be interesting to measure this using the GitHub API. +A: The purpose of these classes is really quite similar. However, I felt that inheriting from `NoneType` could break existing code, which might expect that only one `NoneType` instance exists, and therefore uses the `isinstance` check as an analog of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. It would be interesting to measure this using the GitHub API. Q7: How is the uniqueness of `InnerNoneType` objects ensured? -A: If you create `InnerNoneType` objects without passing any arguments to the constructor, an ID that is unique within the process is created inside each object when it is created. It is by this ID that the object will check whether it is equal to another `InnerNoneType` object. It will be equal to another object only if it has the same ID inside it, which is usually impossible, and therefore the object remains equal only to itself. If you passed your own ID when creating the object, the automatic ID is not created, yours is used. In this case, it is your job to track possible collisions. The library can also distinguish between objects where the ID is created automatically and where it is passed from outside, using a special flag inside each value. This guarantees that there are no intersections between automatically generated and non-automatically generated IDs. +A: If you create `InnerNoneType` objects without passing any arguments to the constructor, each object receives a process-unique ID at creation time. It is by this ID that the object will check whether it is equal to another `InnerNoneType` object. It will be equal to another object only if it has the same ID inside it, which is usually impossible, and therefore the object remains equal only to itself. If you passed your own ID when creating the object, the automatic ID is not created, yours is used. In this case, it is your job to track possible collisions. The library can also distinguish between objects where the ID is created automatically and where it is passed from outside, using a special flag inside each value. This guarantees that there are no intersections between automatically generated and non-automatically generated IDs. Q8: Why all these complications and an additional library for sentinels? I just write `sentinel = object()` in my code and then do checks like `x is sentinel`. It works, but you've overcomplicated things. -A: Indeed, we already have one source of unique IDs for objects: their addresses in memory. Checks like `x is sentinel` can be identical in meaning to those used in this library. However, this option has two significant drawbacks. First, you lose the compactness of string representation that `denial` provides. Second, this method does not allow you to create two identical sentinel objects if you want to, which prevents you from, for example, transferring sentinel objects over the network or between processes. Unfortunately, this is impossible with memory addresses. Since this library is positioned as universal, I had to abandon this option. +A: Indeed, we already have one source of unique IDs for objects: their addresses in memory. Checks like `x is sentinel` can be semantically equivalent to those used in this library. However, this option has two significant drawbacks. First, you lose the compactness of string representation that `denial` provides. Second, this method does not allow you to create two identical sentinel objects if you want to, which prevents you from, for example, transferring sentinel objects over the network or between processes. Unfortunately, this is impossible with memory addresses. Since this library is positioned as universal, I had to abandon this option. Q9: Why don't we use [Enums](https://docs.python.org/3/library/enum.html) as sentinels? It's already in the standard library, no need to invent anything. And it can do all the things we expect from sentinels. From d85107436ca1f868223a409ee2272578e3327db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Tue, 17 Mar 2026 14:45:00 +0300 Subject: [PATCH 6/6] Fix typo in "InnerNoneType" usage and improve clarity in README --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f7d87c3..54eca3d 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Different programming languages and environments offer [different models](#analo > *Yes, this library was also created by [primitive cultures](https://en.wiktionary.org/wiki/Pythonist#English)* -The first option is almost always sufficient. The `denial` library offers special primitives that cover the second and third options to cover the remaining uncertainty cases in Python: +The first option is almost always sufficient. The `denial` library offers special primitives for the second and third options to address the remaining uncertainty cases in Python: - The first option is built into Python and does not require any third-party libraries: [`None`](https://docs.python.org/3/library/constants.html#None). - The second option is represented by the [`InnerNone`](#the-second-none) constant from `denial`. It is practically the same as `None`, just a second `None`. @@ -70,7 +70,7 @@ You can also use [`instld`](https://github.com/pomponchik/instld) to quickly try ## The second `None` -This library defines an object that is used much like built-in `None`. This is how it is imported: +This library defines an object that is used much like the built-in `None`. This is how it is imported: ```python from denial import InnerNone @@ -234,7 +234,7 @@ Programmers often face [the problem of distinguishing types of uncertainty](#the ![standards](https://imgs.xkcd.com/comics/standards.png) > *Current state of affairs* -Some languages provide more explicit built-in support for this distinction than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. Likely because to the fact that [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only two of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. +Some languages provide more explicit built-in support for this distinction than Python. For example, [JavaScript](https://en.wikipedia.org/wiki/JavaScript) explicitly distinguishes between `undefined` and `null`. Likely because [form](https://en.wikipedia.org/wiki/HTML_form) validation is often written in JS, and it often requires such a distinction. However, this approach is not completely universal, since in the general case the number of layers of uncertainty is infinite, and here there are only two of them. In contrast, `denial` provides both features: the basic [`InnerNone`](#the-second-none) constant for simple cases and the ability to create an unlimited number of [`InnerNoneType`](#your-own-none-objects) instances for complex ones. Other languages, such as [AppleScript](https://en.wikipedia.org/wiki/AppleScript) and [SQL](https://en.wikipedia.org/wiki/SQL), also distinguish several different types of undefined values. A separate category includes languages like [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)), [Haskell](https://en.wikipedia.org/wiki/Haskell), [OCaml](https://en.wikipedia.org/wiki/OCaml), and [Swift](https://en.wikipedia.org/wiki/Swift_(programming_language)), which use algebraic data types. The Python standard library uses at least [15 sentinel objects](https://mail.python.org/archives/list/python-dev@python.org/message/JBYXQH3NV3YBF7P2HLHB5CD6V3GVTY55/): @@ -260,7 +260,7 @@ I wasn't the only one to come to this conclusion; the community also tried to st In addition to `denial`, there are many packages with sentinels on [`PyPI`](https://pypi.org/). For example, there is the [sentinel](https://pypi.org/project/sentinel/) library, but its API seemed overcomplicated to me for such a simple task. The [sentinels](https://pypi.org/project/sentinels/) package is quite simple, but in its internal implementation it also relies on the [global registry](https://github.com/vmalloc/sentinels/blob/37e67ed20d99aa7492e52316e9af7f930b9ac578/sentinels/__init__.py#L11) and has some other implementation issues. The [sentinel-value](https://github.com/vdmit11/sentinel-value) package is very similar to `denial`, but I did not see the possibility of auto-generating sentinel IDs there. Of course, there are other packages that I haven't reviewed here. -And of course, there are still different ways to implement primitive sentinels in your code in a few lines of code without using third-party packages. +And of course, there are still different ways to implement primitive sentinels in just a few lines without using third-party packages. ## FAQ @@ -292,7 +292,7 @@ A: I did this to reduce cognitive load. I haven't seen any cases where a clear d Q6: Why is `InnerNoneType` not inherited from `NoneType`? -A: The purpose of these classes is really quite similar. However, I felt that inheriting from `NoneType` could break existing code, which might expect that only one `NoneType` instance exists, and therefore uses the `isinstance` check as an analog of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. It would be interesting to measure this using the GitHub API. +A: These classes serve very similar purposes. However, I felt that inheriting from `NoneType` could break existing code, which might expect that only one `NoneType` instance exists, and therefore uses the `isinstance` check as an analog of the `is None` check. However, I cannot give figures on how often such constructions occur in existing code. It would be interesting to measure this using the GitHub API. Q7: How is the uniqueness of `InnerNoneType` objects ensured? @@ -304,8 +304,8 @@ A: Indeed, we already have one source of unique IDs for objects: their addresses Q9: Why don't we use [Enums](https://docs.python.org/3/library/enum.html) as sentinels? It's already in the standard library, no need to invent anything. And it can do all the things we expect from sentinels. -A: [Various](https://t.me/ru_python/2680685) [people](https://www.reddit.com/r/Python/comments/1qraodv/comment/o2n0d94/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) have suggested this method to me, and it was also mentioned in [PEP-661](https://peps.python.org/pep-0661/). The PEP argues that Enum's `__repr__` is too long. In `denial`, I made a short and informative `__repr__`, which should be sufficient in principle. However, here are some other reasons not to use Enum: 1. Denial can be used in recursive algorithms where the number of nesting levels is unknown in advance. This is possible because the number of variables with sentinels is not limited here. In the case of Enum, you must know the number of nesting levels in advance. 2. Using a single Enum class with all sentinels in the program contradicts the idea of modularity. In essence, this is equivalent to using global variables, which is usually a code smell. If you create an Enum class for each sentinel, it will look too verbose. 3. Under the hood, Enum uses the global registry approach that I discussed in the FAQ section of the README. 4. Enum usually has slightly different semantics. For example, I can hardly imagine a situation where I would want to iterate through all sentinels. 5. Enum is generally [too complex](https://t.me/opensource_findings/883) a tool for such a simple purpose. In my opinion, the entire Enum module should have been deprecated long ago. The sheer size of the module's documentation and the existence of several official manuals for it suggest that something is wrong here. 6. Another argument may seem ridiculous given the slowness of CPython, but I'll mention it anyway. Enum forces the user to use values via a dot, such as `EnumClass.SENTINEL`. This leads to an unnecessary lookup operation and a slight slowdown of the program. However, I haven't done any measurements, so perhaps this has been optimized in some way by the CPython developers. +A: [Various](https://t.me/ru_python/2680685) [people](https://www.reddit.com/r/Python/comments/1qraodv/comment/o2n0d94/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button) have suggested this method to me, and it was also mentioned in [PEP-661](https://peps.python.org/pep-0661/). The PEP argues that Enum's `__repr__` is too long. In `denial`, I made a short and informative `__repr__`, which should be sufficient in principle. However, here are some other reasons not to use Enum: 1. Denial can be used in recursive algorithms where the number of nesting levels is unknown in advance. This is possible because the number of distinct sentinel values is not limited here. In the case of Enum, you must know the number of nesting levels in advance. 2. Using a single Enum class with all sentinels in the program contradicts the idea of modularity. In essence, this is equivalent to using global variables, which is usually a code smell. If you create an Enum class for each sentinel, it will look too verbose. 3. Under the hood, Enum uses the global registry approach that I discussed in the FAQ section of the README. 4. Enum usually has slightly different semantics. For example, I can hardly imagine a situation where I would want to iterate through all sentinels. 5. Enum is generally [too complex](https://t.me/opensource_findings/883) a tool for such a simple purpose. In my opinion, the entire Enum module should have been deprecated long ago. The sheer size of the module's documentation and the existence of several official manuals for it suggest that something is wrong here. 6. Another argument may seem ridiculous given the slowness of CPython, but I'll mention it anyway. Enum forces the user to use values via a dot, such as `EnumClass.SENTINEL`. This leads to an unnecessary lookup operation and a slight slowdown of the program. However, I haven't done any measurements, so perhaps this has been optimized in some way by the CPython developers. Q10: Why does the `singleton` flag prohibit the creation of a second instance if you can simply return the same object? -A: There are different possible implementations of the [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern). In Python code, you often see implementations that “hide” the uniqueness check of an instance under the hood and simply return the first instance when you try to create a second one. But in my opinion, this implementation of the pattern is flawed because it hides the true nature of objects from the reader, which violates [the Zen of Python](https://en.wikipedia.org/wiki/Zen_of_Python) ("explicit is better than implicit"). The code implicitly propagates attempts to create multiple objects of the same class, when in fact only one object is needed. In my opinion, if we want an object to have only one instance, we should explicitly prohibit the creation of more than one instance. In this case, there will be no more than one instance of the class in the code, which makes the code base more consistent and transparent. +A: There are different possible implementations of the [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern). In Python code, you often see implementations that “hide” the uniqueness check behind instance creation and simply return the first instance when you try to create a second one. But in my opinion, this implementation of the pattern is flawed because it hides the true nature of objects from the reader, which violates [the Zen of Python](https://en.wikipedia.org/wiki/Zen_of_Python) ("explicit is better than implicit"). The code implicitly propagates attempts to create multiple objects of the same class, when in fact only one object is needed. In my opinion, if we want an object to have only one instance, we should explicitly prohibit the creation of more than one instance. In this case, there will be no more than one instance of the class in the codebase, which makes the code base more consistent and transparent.