Repository: python-attrs/attrs
Pull request #1358 (2024-10-13 21:09:14)

Introduce NothingType
So I introduced NothingType, but from what I can tell we're not using this anywhere? This is solely for the benefit of third parties annotating their code to accept NOTHING?

hynek: I presume this is based on https://discuss.python.org/t/do-we-need-a-valuealias-in-addition-to-typealias/67609? I guess it needs _some_ docs and a changelog entry?
hynek: _nudge_
hynek: Is there any chance at all to test this??

Patch:
diff --git a/changelog.d/1358.change.md b/changelog.d/1358.change.md
new file mode 100644
index 0000000..6d9a009
--- /dev/null
+++ b/changelog.d/1358.change.md
@@ -0,0 +1,1 @@
+Introduce `attrs.NothingType`, for annotating types consistent with `attrs.NOTHING`.
diff --git a/src/attr/__init__.py b/src/attr/__init__.py
index d2cb920..2e3b7c0 100644
--- a/src/attr/__init__.py
+++ b/src/attr/__init__.py
@@ -5,7 +5,7 @@ Classes Without Boilerplate
 """
 
 from functools import partial
-from typing import Callable, Protocol
+from typing import Callable, Literal, Protocol
 
 from . import converters, exceptions, filters, setters, validators
 from ._cmp import cmp_using
@@ -16,6 +16,7 @@ from ._make import (
     Attribute,
     Converter,
     Factory,
+    _Nothing,
     attrib,
     attrs,
     fields,
@@ -36,12 +37,15 @@ class AttrsInstance(Protocol):
     pass
 
 
+NothingType = Literal[_Nothing.NOTHING]
+
 __all__ = [
     "NOTHING",
     "Attribute",
     "AttrsInstance",
     "Converter",
     "Factory",
+    "NothingType",
     "asdict",
     "assoc",
     "astuple",
diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi
index 47951ee..133e501 100644
--- a/src/attr/__init__.pyi
+++ b/src/attr/__init__.pyi
@@ -5,6 +5,7 @@ from typing import (
     Any,
     Callable,
     Generic,
+    Literal,
     Mapping,
     Protocol,
     Sequence,
@@ -37,9 +38,9 @@ from attrs import (
 )
 
 if sys.version_info >= (3, 10):
-    from typing import TypeGuard
+    from typing import TypeGuard, TypeAlias
 else:
-    from typing_extensions import TypeGuard
+    from typing_extensions import TypeGuard, TypeAlias
 
 if sys.version_info >= (3, 11):
     from typing import dataclass_transform
@@ -72,11 +73,11 @@ class _Nothing(enum.Enum):
     NOTHING = enum.auto()
 
 NOTHING = _Nothing.NOTHING
+NothingType: TypeAlias = Literal[_Nothing.NOTHING]
 
 # NOTE: Factory lies about its return type to make this possible:
 # `x: List[int] # = Factory(list)`
 # Work around mypy issue #4554 in the common case by using an overload.
-from typing import Literal
 
 @overload
 def Factory(factory: Callable[[], _T]) -> _T: ...
diff --git a/src/attr/_make.py b/src/attr/_make.py
index 530ce39..eac5888 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -79,6 +79,8 @@ class _Nothing(enum.Enum):
 NOTHING = _Nothing.NOTHING
 """
 Sentinel to indicate the lack of a value when `None` is ambiguous.
+
+When using in 3rd party code, use `attrs.NothingType` for type annotations.
 """
 
 
diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py
index 3b5ab84..e8023ff 100644
--- a/src/attrs/__init__.py
+++ b/src/attrs/__init__.py
@@ -6,6 +6,7 @@ from attr import (
     AttrsInstance,
     Converter,
     Factory,
+    NothingType,
     _make_getattr,
     assoc,
     cmp_using,
@@ -32,6 +33,7 @@ __all__ = [
     "AttrsInstance",
     "Converter",
     "Factory",
+    "NothingType",
     "__author__",
     "__copyright__",
     "__description__",
diff --git a/src/attrs/__init__.pyi b/src/attrs/__init__.pyi
index 05e5a0c..648fa7a 100644
--- a/src/attrs/__init__.pyi
+++ b/src/attrs/__init__.pyi
@@ -40,6 +40,7 @@ from attr import setters as setters
 from attr import validate as validate
 from attr import validators as validators
 from attr import attrib, asdict as asdict, astuple as astuple
+from attr import NothingType as NothingType
 
 if sys.version_info >= (3, 11):
     from typing import dataclass_transform


Test patch:
diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml
index 8042ebb..41c5029 100644
--- a/tests/test_mypy.yml
+++ b/tests/test_mypy.yml
@@ -1472,3 +1472,16 @@
     reveal_type(A)  # N: Revealed type is "def () -> main.A"
     if has(A):
         reveal_type(A)  # N: Revealed type is "type[attr.AttrsInstance]"
+
+- case: testNothingType
+  regex: true
+  main: |
+    from typing import Optional
+    from attrs import NOTHING, NothingType
+
+    def takes_nothing(arg: Optional[NothingType]) -> None:
+        return None
+
+    takes_nothing(NOTHING)
+    takes_nothing(None)
+    takes_nothing(1)  # E: Argument 1 to "takes_nothing" has incompatible type "Literal\[1\]"; expected "(Optional\[Literal\[_Nothing.NOTHING\]\]|Literal\[_Nothing.NOTHING\] \| None)"  \[arg-type\]