Skip to content

Handle arg=<sentinel value> in stubgenc by generating an ellipsis default annotated as Incomplete - #22019

Open
edgarrmondragon wants to merge 4 commits into
python:masterfrom
edgarrmondragon:stubgen-sentinel
Open

edgarrmondragon wants to merge 4 commits into
python:masterfrom
edgarrmondragon:stubgen-sentinel

Conversation

@edgarrmondragon

Copy link
Copy Markdown
Contributor

mypy was generating stubs like def test(self, arg0: sentinel = ...) -> None: ..., where the sentinel annotation isn't really helpful.

Related:

@edgarrmondragon edgarrmondragon changed the title Handle arg=<sentinen value> in stubgenc Handle arg=<sentinel value> in stubgenc Sep 21, 2026
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon edgarrmondragon changed the title Handle arg=<sentinel value> in stubgenc Handle arg=<sentinel value> in stubgenc by generating an ellipsis default annotated as Incomplete Sep 21, 2026
@edgarrmondragon
edgarrmondragon marked this pull request as ready for review September 21, 2026 18:58

@JelleZijlstra JelleZijlstra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only stubgenc, should we do something similar for regular stubgen?

Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
@edgarrmondragon

Copy link
Copy Markdown
Contributor Author

This is only stubgenc, should we do something similar for regular stubgen?

It seems that it's handled by stubgenc in the --inspect-mode case:

mypy/mypy/stubgen.py

Lines 1811 to 1820 in dc8858f

if inspect:
ngen = InspectionStubGenerator(
module_name=mod.module,
known_modules=all_modules,
_all_=mod.runtime_all,
doc_dir=doc_dir,
include_private=include_private,
export_less=export_less,
include_docstrings=include_docstrings,
)

I've added both a testDefaultArgSentinel and testDefaultArgSentinel_inspect to check both cases. I don't think the former infers runtime default values, so that default case in unaffected.

dc8858f

@ilevkivskyi

Copy link
Copy Markdown
Member

@JukkaL @p-sawicki It looks like testConcurrentCircularNativeImports is flaky, see failure in https://github.com/python/mypy/actions/runs/35688997253/job/106621787371?pr=22019

Expected:
Actual:
  Traceback (most recent call last): (diff)
    File "driver.py", line 7, in <module> (diff)
      other_a = future_a.result(timeout=15) (diff)
    File "/opt/hostedtoolcache/Python/3.14.7/arm64-freethreaded/lib/python3.14t/concurrent/futures/_base.py", line 454, in result (diff)
      return self.__get_result() (diff)
             ~~~~~~~~~~~~~~~~~^^ (diff)
    File "/opt/hostedtoolcache/Python/3.14.7/arm64-freethreaded/lib/python3.14t/concurrent/futures/_base.py", line 396, in __get_result (diff)
      raise self._exception (diff)
    File "/opt/hostedtoolcache/Python/3.14.7/arm64-freethreaded/lib/python3.14t/concurrent/futures/thread.py", line 86, in run (diff)
      result = ctx.run(self.task) (diff)
    File "/opt/hostedtoolcache/Python/3.14.7/arm64-freethreaded/lib/python3.14t/concurrent/futures/thread.py", line 73, in run (diff)
      return fn(*args, **kwargs) (diff)
    File "/opt/hostedtoolcache/Python/3.14.7/arm64-freethreaded/lib/python3.14t/importlib/__init__.py", line 88, in import_module (diff)
      return _bootstrap._gcd_import(name[level:], package, level) (diff)
             ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (diff)
    File "<frozen importlib._bootstrap>", line 1406, in _gcd_import (diff)
    File "<frozen importlib._bootstrap>", line 1371, in _find_and_load (diff)
    File "<frozen importlib._bootstrap>", line 1342, in _find_and_load_unlocked (diff)
    File "<frozen importlib._bootstrap>", line 938, in _load_unlocked (diff)
    File "<frozen importlib._bootstrap_external>", line 1061, in exec_module (diff)
    File "<frozen importlib._bootstrap>", line 491, in _call_with_frames_removed (diff)
    File "other_a.py", line 7, in <module> (diff)
      import other_b (diff)
  KeyError: 'other_b' (diff)

Comment thread test-data/unit/stubgen.test Outdated
[out]
from _typeshed import Incomplete

def f(x: Incomplete = ...): ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel ideally we should generate a stub for the sentinel here and annotate the parameter as Incomplete | _MISSING.

@edgarrmondragon edgarrmondragon Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel ideally we should generate a stub for the sentinel here and annotate the parameter as Incomplete | _MISSING.

Do you mean specifically for private sentinel variable? AFAICT stubgen skips runtime introspection of types for private variables:

mypy/mypy/stubgenc.py

Lines 910 to 917 in b645e06

def generate_variable_stub(self, name: str, obj: object, output: list[str]) -> None:
"""Generate stub for a single variable using runtime introspection.
The result lines will be appended to 'output'. If necessary, any
required names will be added to 'imports'.
"""
if self.is_private_name(name, f"{self.module_name}.{name}") or self.is_not_in_all(name):
return

I can see the value in generating Incomplete | MISSING though, i.e. doing runtime introspection of a public sentinel.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could go either way with private sentinels, but definitely for public sentinels we should be doing this.

Even for private sentinels, as a typeshed maintainer I'd prefer having the sentinel in the stub for code like this. Others might disagree though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e82ca74, but I do have a question about generating backward compatible stubs. A sentinel can be created using

  • builtins.sentinel/sentinel: Python 3.15+
  • typing_extensions.sentinel: typing_extensions 4.16.0+
  • typing_extensions.Sentinel: typing_extensions 4.14.0+, soft deprecated in 4.16.0

but perhaps a stub should be as version-agnostic as possible? That is, use typing_extensions.Sentinel?

Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Comment thread mypy/stubgenc.py
# dotted form (not add_name) since a module constructing a
# sentinel will itself have already imported the bare `sentinel`
# name, which would otherwise force an alias here.
return "typing_extensions.sentinel"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is typing-extensions 4.16.0+. Should we bump the min requirement here?

mypy/pyproject.toml

Lines 56 to 57 in b645e06

"typing_extensions>=4.6.0; python_version<'3.15'",
"typing_extensions>=4.14.0; python_version>='3.15'",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants