Repository: h4l/sortedcontainers-stubs (language: python)

Issue:
Problem using typed sortedcontainer with defaultfactory
so with dataclass, it is supposed to use default_factory to create stuff, I can't really find a way to make annotation without problems

sample code:
```
from dataclasses import dataclass, field
from sortedcontainers import SortedList

@dataclass
class Example():
    samples: SortedList[str] = field(default_factory=SortedList)
```

error:
```
Argument of type "type[SortedList[_T@SortedList]]" cannot be assigned to parameter "default_factory" of type "() -> _T@field" in function "field"
  No overloaded function matches type "() -> SortedList[str]"
```


Discussion / hints:
Thanks for the report. It's probably because of the way `SortedList.__new__` has several overloads but no explicit empty argument overload. I'll see if I can tweak the overloads to make this work.

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 70c4769..c39982d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -17,7 +17,6 @@ jobs:
     outputs:
       test_targets: ${{ steps.expand_bake_matrix.outputs.test_targets }}
       lint_targets: ${{ steps.expand_bake_matrix.outputs.lint_targets }}
-      mypy_targets: ${{ steps.expand_bake_matrix.outputs.mypy_targets }}
 
     steps:
       - name: Checkout
@@ -30,7 +29,7 @@ jobs:
         id: expand_bake_matrix
         run: |
           docker buildx bake --print \
-            | jq -cer '{test: .group.test.targets, lint: .group.lint.targets, mypy: .group.mypy.targets}
+            | jq -cer '{test: .group.test.targets, lint: .group.lint.targets}
                        | to_entries[]
                        | {name: .key, targets: .value}
                        | "\(.name)_targets=\(.targets | tojson)"' \
@@ -85,28 +84,3 @@ jobs:
           set: |
             *.cache-from=type=gha
             *.cache-to=type=gha,mode=max
-
-  mypy:
-    name: Run mypy under several Python versions
-    runs-on: ubuntu-latest
-    needs:
-      - expand_bake_matrix
-    strategy:
-      matrix:
-        target: ${{ fromJSON(needs.expand_bake_matrix.outputs.mypy_targets) }}
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
-
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@4c0219f9ac95b02789c1075625400b2acbff50b1
-
-      - name: Run Lint Matrix
-        uses: docker/bake-action@f32f8b8d70bc284af19f8148dd14ad1d2fbc6c28
-        with:
-          targets: ${{ matrix.target }}
-          files: docker-bake.hcl
-          set: |
-            *.cache-from=type=gha
-            *.cache-to=type=gha,mode=max
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c26d78..1729640 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,28 @@ minor version; the patch version increments independently to release fixes.
 
 ## [Unreleased]
 
-- Nothing yet
+### Fixed
+
+- Using the SortedList (and the other) types as a function that creates an
+  instance of the type works in pyright now.
+  ([#3](https://github.com/h4l/sortedcontainers-stubs/issues/3))
+
+### Changed
+
+- Because of the fix to allow pyright to allow using the types as functions, the
+  constructors with no arguments had to be widened to permit assignment to
+  non-comparable/hashable types. The result is that it's unfortunately not a
+  type error to do something like `sl: SortedList[type] = SortedList()` even
+  though subsequently adding multiple values to this list will fail at runtime.
+
+  It doesn't seem to be possible to type the signature of `__new__` to constrain
+  `SortedList()` to only allow comparable element types in a standard way (mypy
+  supported the previous method which worked, but other checkers don't).
+
+- The signature definition of SortedDict.setdefault was restructured to satisfy
+  pyright, but the effect of the signature itself remains the same.
+
+- pyright runs in CI now
 
 ## [2.4.1] — 2023-10-10
 
diff --git a/Dockerfile b/Dockerfile
index 5b92546..9ba48cd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,24 +4,28 @@ FROM python:${PYTHON_VER:?} AS python-base
 
 
 FROM python-base AS poetry
-RUN pip install poetry
+RUN --mount=type=cache,target=/root/.cache pip install poetry
+RUN python -m venv /venv
+ENV VIRTUAL_ENV=/venv \
+    PATH="/venv/bin:$PATH"
+RUN poetry config virtualenvs.create false
 WORKDIR /workspace
 COPY pyproject.toml poetry.lock /workspace/
 
 # Poetry needs these to exist to setup the editable install
 RUN mkdir sortedcontainers-stubs && touch README.md
-RUN poetry install
+RUN --mount=type=cache,target=/root/.cache poetry install
 
 
 FROM poetry AS test
 RUN --mount=source=.,target=/workspace,rw \
     --mount=type=cache,uid=1000,target=.pytest_cache \
-    poetry run pytest
+    pytest
 
 
 FROM poetry AS lint-flake8
 RUN --mount=source=.,target=/workspace,rw \
-    poetry run flake8
+    flake8
 
 
 FROM poetry AS lint-black
@@ -38,3 +42,15 @@ FROM poetry AS lint-mypy
 RUN --mount=source=.,target=/workspace,rw \
     --mount=type=cache,target=.mypy_cache \
     poetry run mypy .
+
+
+FROM python-base AS pyright
+RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm
+RUN npm install -g pyright
+WORKDIR /workspace
+COPY --link --from=poetry /venv /venv
+
+
+FROM pyright AS lint-pyright
+RUN --mount=source=.,target=/workspace,rw \
+    pyright --pythonpath /venv/bin/python .
diff --git a/docker-bake.hcl b/docker-bake.hcl
index 73ccb4b..11a5ccd 100644
--- a/docker-bake.hcl
+++ b/docker-bake.hcl
@@ -1,5 +1,5 @@
 group "default" {
-    targets = ["test", "lint", "mypy"]
+    targets = ["test", "lint"]
 }
 
 target "test" {
@@ -15,23 +15,10 @@ target "test" {
     output = ["type=cacheonly"]
 }
 
-target "mypy" {
-    name = "mypy_python-${replace(py, ".", "-")}"
-    matrix = {
-        py = ["3.8", "3.9", "latest"],
-    }
-    args = {
-        PYTHON_VER = py == "latest" ? "slim" : "${py}-slim"
-    }
-    target = "lint-mypy"
-    no-cache-filter = ["lint-mypy"]
-    output = ["type=cacheonly"]
-}
-
 target "lint" {
     name = "lint-${lint_type}"
     matrix = {
-        lint_type = ["flake8", "black", "isort"],
+        lint_type = ["flake8", "black", "isort", "mypy", "pyright"],
     }
     args = {
         PYTHON_VER = "slim"
diff --git a/pyproject.toml b/pyproject.toml
index e3b05de..54ed13e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,3 +56,7 @@ extra_standard_library = ["typing_extensions"]
 
 [tool.mypy]
 strict = true
+
+[tool.pyright]
+include = ["sortedcontainers-stubs", "test"]
+strict = ["sortedcontainers-stubs", "test"]
diff --git a/sortedcontainers-stubs/_typing.pyi b/sortedcontainers-stubs/_typing.pyi
index 74a5d1f..f7be9d4 100644
--- a/sortedcontainers-stubs/_typing.pyi
+++ b/sortedcontainers-stubs/_typing.pyi
@@ -1,8 +1,10 @@
 from collections.abc import Hashable
-from typing import Any, Protocol
+from typing import Any, Protocol, TypeVar
 from typing_extensions import TypeAlias
 
-from _typeshed import SupportsDunderGT, SupportsDunderLT, _T_contra  # noqa: F401
+from _typeshed import SupportsDunderGT, SupportsDunderLT
+
+_T_contra = TypeVar("_T_contra", contravariant=True)
 
 class SupportsHashableAndDunderGT(
     Hashable, SupportsDunderGT[_T_contra], Protocol[_T_contra]
diff --git a/sortedcontainers-stubs/sorteddict.pyi b/sortedcontainers-stubs/sorteddict.pyi
index a08587a..09f6390 100644
--- a/sortedcontainers-stubs/sorteddict.pyi
+++ b/sortedcontainers-stubs/sorteddict.pyi
@@ -39,10 +39,15 @@ _OrderT = TypeVar(
 KeyFunc: TypeAlias = Callable[[_KT], _OrderT]
 
 class SortedDict(MutableMapping[_KT, _VT]):
+    # mypy 1.5.1 complains about overlapping overloaded function signatures with
+    # incompatible return types. This seems to be because the signatures using
+    # kwargs can would overlap when no kwargs are present, but there's no way to
+    # type a non-empty kwargs param.
+
+    # Should return SortedDict[_OrderT, _VT] but we can't reliably express that.
+    # See comment on SortedList.__new__.
     @overload
-    def __new__(
-        cls,
-    ) -> SortedDict[_OrderT, _VT]: ...
+    def __new__(cls) -> Self: ...  # type: ignore[misc]
     @overload
     def __new__(
         cls,
@@ -69,17 +74,6 @@ class SortedDict(MutableMapping[_KT, _VT]):
         /,
         **kwargs: _VT,
     ) -> SortedKeyDict[str, _VT, _OrderT]: ...
-    # mypy 1.5.1 complains: "Overloaded function signatures 5 and 6 overlap with
-    # incompatible return types  [misc]". If no kwargs are passed, they would
-    # indeed overlap, but there's no way to specify non-empty kwargs. We need
-    # this overload to prevent kwargs adding str keys to non-str __entries.
-    #
-    # Type-enforcement seems to work correct despite the error we ignore here...
-    # Alternatively, we would need to not type calls with both entries and
-    # kwargs.
-    #
-    #  Ideally str would be "str or a superclass of str", but I can't find a way
-    # to type that.
     @overload
     def __new__(  # type: ignore[misc]
         cls,
@@ -125,7 +119,10 @@ class SortedDict(MutableMapping[_KT, _VT]):
     def pop(self, key: _KT, /, default: _T) -> _VT | _T: ...
     def popitem(self, index: int = ...) -> tuple[_KT, _VT]: ...
     def peekitem(self, index: int = ...) -> tuple[_KT, _VT]: ...
-    def setdefault(self, key: _KT, default: _VT = ...) -> _VT: ...
+    @overload
+    def setdefault(self: MutableMapping[_KT, _T | None], key: _KT) -> _T | None: ...
+    @overload
+    def setdefault(self, key: _KT, default: _VT) -> _VT: ...
     # Our override of update is strictly incompatible in that we only allow
     # kwargs when _KT is str. This is consistent with dict()'s constructor
     # typing, and allowing kwargs with non-str _KT causes TypeErrors at runtime.
@@ -146,7 +143,7 @@ class SortedDict(MutableMapping[_KT, _VT]):
     @overload
     def update(self, __iterable: Iterable[tuple[_KT, _VT]]) -> None: ...
     @overload
-    def update(self: SortedDict[str, _VT], **kwargs: _VT) -> None: ...
+    def update(self: SortedDict[str, _VT], **kwargs: _VT) -> None: ...  # type: ignore[override,unused-ignore]
     def __reduce__(
         self,
     ) -> tuple[
diff --git a/sortedcontainers-stubs/sortedlist.pyi b/sortedcontainers-stubs/sortedlist.pyi
index f3fe0be..1cf8883 100644
--- a/sortedcontainers-stubs/sortedlist.pyi
+++ b/sortedcontainers-stubs/sortedlist.pyi
@@ -19,8 +19,15 @@ class SortedList(MutableSequence[_T]):
     DEFAULT_LOAD_FACTOR: Final[int] = ...
     @overload
     def __new__(cls, key: KeyFunc[_T, _OrderT]) -> SortedKeyList[_T, _OrderT]: ...
+    # Returning SortedList[Any] is a compromise.
+    # The signature should be something like:
+    # (cls: type[SortedList[_OrderT]], ...) -> SortedList[_OrderT]
+    # but neither mypy nor pyright support typing cls like this. If we leave cls
+    # bare then mypy correctly constrains the return type to only allow _OrderT
+    # elements, but pylance doesn't like the solo type argument, and does not
+    # allow the type to be used as () -> SortedList[...].
     @overload
-    def __new__(cls, iterable: None = ..., key: None = ...) -> SortedList[_OrderT]: ...
+    def __new__(cls, iterable: None = ..., key: None = ...) -> Self: ...
     @overload
     def __new__(
         cls, iterable: Iterable[_OrderT], key: None = ...
diff --git a/sortedcontainers-stubs/sortedset.pyi b/sortedcontainers-stubs/sortedset.pyi
index 0bf4085..27c9538 100644
--- a/sortedcontainers-stubs/sortedset.pyi
+++ b/sortedcontainers-stubs/sortedset.pyi
@@ -21,8 +21,10 @@ class SortedSet(MutableSet[_T], Sequence[_T]):
     def __new__(cls, iterable: Iterable[_OrderT]) -> SortedSet[_OrderT]: ...
     @overload
     def __new__(cls, key: KeyFunc[_T, _OrderT]) -> SortedKeySet[_T, _OrderT]: ...
+    # Should return SortedSet[_OrderT] but we can't reliably express that.
+    # See comment on SortedList.__new__.
     @overload
-    def __new__(cls, iterable: None = ..., key: None = ...) -> SortedSet[_OrderT]: ...
+    def __new__(cls, iterable: None = ..., key: None = ...) -> Self: ...
     @overload
     def __new__(cls, iterable: Iterable[_OrderT], key: None) -> SortedSet[_OrderT]: ...
     @overload


Test patch:
diff --git a/test/test_sorteddict.py b/test/test_sorteddict.py
index 34bd051..74e21d9 100644
--- a/test/test_sorteddict.py
+++ b/test/test_sorteddict.py
@@ -1,7 +1,9 @@
 from __future__ import annotations
 
+from dataclasses import dataclass, field
 from typing import TYPE_CHECKING
 
+import pytest
 from sortedcontainers import SortedDict
 
 if TYPE_CHECKING:
@@ -11,6 +13,26 @@
     from sortedcontainers.sorteddict import SortedKeyDict
 
 
+def test_constructor_references() -> None:
+    def str_to_int(x: str) -> int:
+        return int(x)
+
+    @dataclass
+    class Example:
+        samples: SortedDict[str, float] = field(default_factory=SortedDict)
+        named_samples: SortedKeyDict[str, float, int] = field(
+            default_factory=lambda: SortedDict(str_to_int)
+        )
+
+    e = Example()
+    e.samples["b"] = 1.2
+    e.samples["a"] = 5.0
+    assert list(e.samples) == ["a", "b"]
+    e.named_samples["40"] = 1.2
+    e.named_samples["5"] = 5.0
+    assert list(e.named_samples) == ["5", "40"]
+
+
 def test_sorted_key_dict() -> None:
     def str_to_int(x: str) -> int:
         return int(x)
@@ -26,3 +48,13 @@ def str_to_int(x: str) -> int:
     sorted_dict: SortedDict[str, float] = SortedDict({"2": 12.3, "10": 44.2})
     assert sorted_dict.key is None
     assert not hasattr(sorted_dict, "bisect_key_left")
+
+
+def test_unavoidable_type_violations() -> None:
+    # See SortedList's constructor (__new__()) for more on the same issue.
+    # We can't constrain the key type to be hashable & orderable when no initial
+    # values are specified.
+    broken: SortedDict[type, int] = SortedDict()
+    broken[str] = 42
+    with pytest.raises(TypeError):
+        broken[bytes] = 12
diff --git a/test/test_sorteddict.yml b/test/test_sorteddict.yml
index e7de75f..42538ca 100644
--- a/test/test_sorteddict.yml
+++ b/test/test_sorteddict.yml
@@ -24,6 +24,30 @@
     reveal_type(SortedDict(str_to_int, [('a', 1.2)]))         # N: Revealed type is "sortedcontainers.sorteddict.SortedKeyDict[builtins.str, builtins.float, builtins.int]"
     #reveal_type(SortedDict(str_to_int, [('a', 1.2)], a=1.2))  # N_ Revealed type is "sortedcontainers.sorteddict.SortedKeyDict[builtins.str, builtins.float, builtins.int]"
 
+- case: constructor_references
+  main: |
+    from dataclasses import dataclass, field
+    from sortedcontainers import SortedDict
+    from sortedcontainers.sorteddict import SortedKeyDict
+
+    def str_to_int(x: str) -> int:
+        return int(x)
+
+    @dataclass
+    class Example():
+        samples: SortedDict[str, float] = field(default_factory=SortedDict)
+        named_samples: SortedKeyDict[str, float, int] = field(
+            default_factory=lambda: SortedDict(str_to_int)
+        )
+
+    @dataclass
+    class InvalidExample():
+        named_samples: SortedKeyDict[float, float, int] = field(
+            default_factory=lambda: SortedDict(str_to_int)
+        )
+  out: |
+    main:18: error: Argument "default_factory" to "field" has incompatible type "Callable[[], SortedKeyDict[str, float, int]]"; expected "Callable[[], SortedKeyDict[float, float, int]]"  [arg-type]
+    main:18: error: Incompatible return value type (got "SortedKeyDict[str, float, int]", expected "SortedKeyDict[float, float, int]")  [return-value]
 
 - case: constructor_errors
   main: |
@@ -62,6 +86,21 @@
   out: |
     main:15: error: List item 0 has incompatible type "tuple[float, str]"; expected "tuple[str, str]"  [list-item]
 
+# We'd like these to be errors, but can't type them at the moment
+- case: constructor_type_issues_1
+  regex: false
+  main: |
+    from sortedcontainers import SortedDict
+
+    # This would fail when adding > 1 value at runtime
+    non_comparable_elements1: SortedDict[type, int] = SortedDict()
+
+    # However, we do type it correctly when values are provided to the constructor
+    non_comparable_elements2: SortedDict[type, int] = SortedDict([(str, 42)])
+  out: |
+    main:7: error: Value of type variable "_OrderT" of "SortedDict" cannot be "type[str]"  [type-var]
+    main:7: error: Incompatible types in assignment (expression has type "SortedDict[type[str], int]", variable has type "SortedDict[type, int]")  [assignment]
+
 
 - case: mutable_mapping_methods
   main: |
diff --git a/test/test_sortedlist.py b/test/test_sortedlist.py
index 73ef6fd..967ffc4 100644
--- a/test/test_sortedlist.py
+++ b/test/test_sortedlist.py
@@ -1,10 +1,11 @@
 from __future__ import annotations
 
 import sys
-from typing import Container, TypeVar
+from dataclasses import dataclass, field
+from typing import Callable, Container, TypeVar
 
 import pytest
-from sortedcontainers import SortedList
+from sortedcontainers import SortedKeyList, SortedList
 
 T = TypeVar("T")
 
@@ -28,16 +29,21 @@ def test_creation_py38() -> None:
     use_sortedlist(object, SortedList(key=id))
 
 
-@not_py38
-def test_creation() -> None:
-    # It's possible to subscript SortedList to specify types, despite the actual
-    # implementation not being typed. SortedList needs two parameters, because
-    # the constructor can return a SortedKeyList which has two. Best not to do
-    # this and infer the type via assignment or arguments instead.
-    use_sortedlist(int, SortedList[None, int]([]))
-    sl = SortedList[None, int]()
-    sl.add(1)
-    assert sl == [1]
+def test_constructor_references() -> None:
+    create: Callable[[], SortedList[int]] = SortedList
+    assert create() == SortedList()
+
+    @dataclass
+    class Example:
+        things1: SortedList[str] = field(default_factory=SortedList)
+        things2: SortedKeyList[type, str] = field(
+            default_factory=lambda: SortedKeyList(key=str)
+        )
+
+    e = Example()
+    e.things1.add("a")
+    e.things2.add(str)
+    e.things2.add(int)
 
 
 def test_unavoidable_type_violations() -> None:
@@ -54,13 +60,25 @@ def test_unavoidable_type_violations() -> None:
         "42" in sl  # type: ignore[operator]
 
     with pytest.raises(TypeError):
-        # Not a type error when using Container
-        c.__contains__("42")
+        # Not a type error when using Container (for mypy at least, pyright uses
+        # the element type, which is not what the pyi files actually declare).
+        c.__contains__("42")  # pyright: ignore
 
     with pytest.raises(TypeError):
         # mypy can detect a possible error via comparison-overlap though
         "42" in c  # type: ignore[comparison-overlap]
 
+    # __new__ is typed to return SortedList[Any] when no args are given because
+    # support for a single generic param in the return position is variable.
+    # MyPy supports it, but pyright doesn't. It doesn't seem to be explicitly
+    # supported by typing PEPs.
+    # As a result, we can create empty SortedList with non-comparable types,
+    # which will fail when values are added at runtime.
+    types: SortedList[type] = SortedList()
+    types.add(str)
+    with pytest.raises(TypeError):
+        types.add(str)
+
 
 def test_operators() -> None:
     s = SortedList([1])
diff --git a/test/test_sortedlist.yml b/test/test_sortedlist.yml
index 0e09725..814026f 100644
--- a/test/test_sortedlist.yml
+++ b/test/test_sortedlist.yml
@@ -1,3 +1,18 @@
+# Because __new__'s overloads use 0, 1 or 2 two generic types for different
+# overloads, mypy gets confused and complains that specifying 1 type arg is
+# too few, and 2 is too many. So it's not possible to subscript the
+# constructor. Instead, specify the type of the thing the constructor is
+# assigned to.
+- case: constructor_cannot_be_subscripted_with_1_param
+  main: |
+    from sortedcontainers import SortedList
+    SortedList[int]() # E: Type application has too few types (2 expected)  [misc]
+
+- case: constructor_cannot_be_subscripted_with_2_params
+  main: |
+    from sortedcontainers import SortedList
+    SortedList[None, int]() # E: Type application has too many types (1 expected)  [misc]
+
 - case: constructor
   main: |
     from sortedcontainers import SortedList
@@ -7,12 +22,6 @@
     l: SortedList[int] = SortedList()
     reveal_type(l)                      # N: Revealed type is "sortedcontainers.sortedlist.SortedList[builtins.int]"
 
-    # Because __new__'s overloads use two generic types, both need to be
-    # specified when subscripting a constructor call. I'd recommend not doing
-    # this and relying on inference via assignment or parameter types.
-    # (The first type is T which is not used in this overload.)
-    reveal_type(SortedList[None, int]())  # N: Revealed type is "sortedcontainers.sortedlist.SortedList[builtins.int]"
-
     def str_to_int(x: str) -> int:
       return int(x)
 
@@ -40,6 +49,27 @@
     reveal_type(SortedKeyList(None, str_to_int))  # N: Revealed type is "sortedcontainers.sortedlist.SortedKeyList[builtins.str, builtins.int]"
     reveal_type(SortedKeyList(key=str_to_int))    # N: Revealed type is "sortedcontainers.sortedlist.SortedKeyList[builtins.str, builtins.int]"
 
+- case: constructor_references
+  main: |
+    from dataclasses import dataclass, field
+    from sortedcontainers import SortedList, SortedKeyList
+
+    def str_to_int(x: str) -> int:
+        return int(x)
+
+    @dataclass
+    class Example():
+        samples: SortedList[str] = field(default_factory=SortedList)
+        named_samples: SortedKeyList[str, int] = field(
+            default_factory=lambda: SortedKeyList(key=str_to_int)
+        )
+
+    @dataclass
+    class InvalidExample():
+        named_samples: SortedKeyList[float, int] = field(
+            default_factory=lambda: SortedKeyList(key=str_to_int)  # E: Argument "key" to "SortedKeyList" has incompatible type "Callable[[str], int]"; expected "Callable[[float], int]"  [arg-type]
+        )
+
 - case: constructor_errors
   regex: true
   main: |
@@ -54,6 +84,35 @@
     # Not comparable
     SortedList([object()])  # E: Value of type variable "_OrderT" of "SortedList" cannot be "object"  \[type-var\]
 
+# We'd like these to be errors, but can't type them at the moment
+- case: constructor_type_issues_1
+  regex: false
+  main: |
+    from sortedcontainers import SortedList, SortedKeyList
+
+    # This would fail when adding > 1 value at runtime
+    non_comparable_elements1: SortedList[type] = SortedList()
+
+    # However, we do type it correctly when values are provided to the constructor
+    non_comparable_elements2: SortedList[type] = SortedList([str])
+  out: |
+    main:7: error: Value of type variable "_OrderT" of "SortedList" cannot be "type[str]"  [type-var]
+    main:7: error: Incompatible types in assignment (expression has type "SortedList[type[str]]", variable has type "SortedList[type]")  [assignment]
+
+- case: constructor_type_issues_2
+  regex: false
+  main: |
+    from sortedcontainers import SortedList, SortedKeyList
+
+    # This would fail when adding > 1 value at runtime
+    non_comparable_elements1: SortedList[type] = SortedKeyList()
+
+    # However, we do type it correctly when values are provided to the constructor
+    non_comparable_elements2: SortedList[type] = SortedKeyList([str])
+  out: |
+    main:7: error: Value of type variable "_OrderT" of "SortedKeyList" cannot be "type[str]"  [type-var]
+    main:7: error: Incompatible types in assignment (expression has type "SortedKeyList[type[str], type[str]]", variable has type "SortedList[type]")  [assignment]
+
 - case: comparison_operators
   main: |
     from sortedcontainers import SortedList
diff --git a/test/test_sortedset.py b/test/test_sortedset.py
index 8d0562a..d6a144d 100644
--- a/test/test_sortedset.py
+++ b/test/test_sortedset.py
@@ -1,8 +1,15 @@
+from __future__ import annotations
+
 import re
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Callable
 
 import pytest
 from sortedcontainers import SortedSet
 
+if TYPE_CHECKING:
+    from sortedcontainers.sortedset import SortedKeySet
+
 
 def test_contains() -> None:
     s = SortedSet(["a", "b"])
@@ -11,6 +18,23 @@ def test_contains() -> None:
     assert "a" in s
 
 
+def test_constructor_references() -> None:
+    create: Callable[[], SortedSet[int]] = SortedSet
+    assert create() == SortedSet()
+
+    @dataclass
+    class Example:
+        things1: SortedSet[str] = field(default_factory=SortedSet)
+        things2: SortedKeySet[type, str] = field(
+            default_factory=lambda: SortedSet(key=str)
+        )
+
+    e = Example()
+    e.things1.add("a")
+    e.things2.add(str)
+    e.things2.add(int)
+
+
 def test_element_addition_type_safety() -> None:
     # Because the implementation needs to compare elements for order, it does
     # not support mixing element types that cannot be compared.
@@ -29,3 +53,13 @@ def test_element_addition_type_safety() -> None:
     ):
         {"a", "b"} | [1, 2]  # type: ignore[operator]
     assert {"a", "b"} | {1, 2} == {"a", "b", 1, 2}
+
+
+def test_unavoidable_type_violations() -> None:
+    # See SortedList's constructor (__new__()) for more on the same issue.
+    # We can't constrain the key type to be hashable & orderable when no initial
+    # values are specified.
+    broken: SortedSet[type] = SortedSet()
+    broken.add(str)
+    with pytest.raises(TypeError):
+        broken.add(bytes)
diff --git a/test/test_sortedset.yml b/test/test_sortedset.yml
index 4f5c52f..60b3bde 100644
--- a/test/test_sortedset.yml
+++ b/test/test_sortedset.yml
@@ -1,8 +1,23 @@
+# Because __new__'s overloads use 0, 1 or 2 two generic types for different
+# overloads, mypy gets confused and complains that specifying 1 type arg is
+# too few, and 2 is too many. So it's not possible to subscript the
+# constructor. Instead, specify the type of the thing the constructor is
+# assigned to.
+- case: constructor_cannot_be_subscripted_with_1_param
+  main: |
+    from sortedcontainers import SortedSet
+    SortedSet[int]() # E: Type application has too few types (2 expected)  [misc]
+
+- case: constructor_cannot_be_subscripted_with_2_params
+  main: |
+    from sortedcontainers import SortedSet
+    SortedSet[None, int]() # E: Type application has too many types (1 expected)  [misc]
+
+
 - case: constructor
   main: |
     from sortedcontainers import SortedSet
 
-    reveal_type(SortedSet[None, int]())  # N: Revealed type is "sortedcontainers.sortedset.SortedSet[builtins.int]"
     reveal_type(SortedSet())             # N: Revealed type is "sortedcontainers.sortedset.SortedSet[<nothing>]"
     reveal_type(SortedSet([1, 2, 3]))    # N: Revealed type is "sortedcontainers.sortedset.SortedSet[builtins.int]"
     l: SortedSet[int] = SortedSet()
@@ -15,6 +30,27 @@
     reveal_type(SortedSet(iterable=None, key=str_to_int))  # N: Revealed type is "sortedcontainers.sortedset.SortedKeySet[builtins.str, builtins.int]"
     reveal_type(SortedSet(key=str_to_int))                 # N: Revealed type is "sortedcontainers.sortedset.SortedKeySet[builtins.str, builtins.int]"
 
+- case: constructor_references
+  main: |
+    from dataclasses import dataclass, field
+    from sortedcontainers import SortedSet
+    from sortedcontainers.sortedset import SortedKeySet
+
+    def str_to_int(x: str) -> int:
+        return int(x)
+
+    @dataclass
+    class Example():
+        samples: SortedSet[str] = field(default_factory=SortedSet)
+        named_samples: SortedKeySet[str, int] = field(
+            default_factory=lambda: SortedSet(key=str_to_int)
+        )
+
+    @dataclass
+    class InvalidExample():
+        named_samples: SortedKeySet[float, int] = field(
+            default_factory=lambda: SortedSet(key=str_to_int)  # E: Argument "key" to "SortedSet" has incompatible type "Callable[[str], int]"; expected "Callable[[float], int]"  [arg-type]
+        )
 
 - case: constructor_errors
   main: |
@@ -40,6 +76,23 @@
     # Hashable but not comparable:
     SortedSet([object()], key=id)  # OK
 
+# We'd like these to be errors, but can't type them at the moment
+- case: constructor_type_issues_1
+  regex: false
+  main: |
+    from sortedcontainers import SortedSet
+    from sortedcontainers.sortedset import SortedKeySet
+
+    # This would fail when adding > 1 value at runtime
+    non_comparable_elements1: SortedSet[type] = SortedSet()
+
+    # However, we do type it correctly when values are provided to the constructor
+    non_comparable_elements2: SortedSet[type] = SortedSet([str])
+  out: |
+    main:8: error: Value of type variable "_OrderT" of "SortedSet" cannot be "type[str]"  [type-var]
+    main:8: error: Incompatible types in assignment (expression has type "SortedSet[type[str]]", variable has type "SortedSet[type]")  [assignment]
+
+
 - case: operators
   main: |
     from sortedcontainers import SortedSet