Skip to content

Handle arg=<sentinel value> in stubgen - #22019

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

edgarrmondragon wants to merge 8 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
@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?

@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
Comment thread mypy/stubgenc.py
Comment thread test-data/unit/stubgen.test Outdated
import typing_extensions
from _typeshed import Incomplete

_MISSING: typing_extensions.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.

This should be emitted as _MISSING = typing_extensions.sentinel("_MISSING"). Type checkers are not expected to understand this form as a sentinel declaration.

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.

Yeah, let me get this back to draft.

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.

Ready for another review. The sentinel object is now assigned instead of annotated. The only edge case where I wasn't able to do it is when the sentinel used a default value is created with a __name__ that is not a valid identifier. For example

MY_SENTINEL = sentinel('not an identifier')

I might be able to do it with vars(...) on the __module__ attribute, but I think that's thinking it too hard for a way to declare sentinels that I believe would be discouraged anyway.

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.

Yes, makes sense!

@edgarrmondragon
edgarrmondragon marked this pull request as draft September 25, 2026 13:00
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez-Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez-Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez-Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez-Mondragón <edgarrm358@gmail.com>
@edgarrmondragon
edgarrmondragon marked this pull request as ready for review September 25, 2026 21:22

@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.

Thanks! I had Astra take a look and got these review findings:


  1. [P2] Sentinel names aren’t necessarily usable type references — lines 570–571 For MISSING = sentinel("missing"), the stub declares MISSING but annotates defaults with Incomplete | missing, producing an undefined-name error. Class-attribute sentinels also produce invalid type references because their declarations remain ClassVar[sentinel]. Verify that the reference resolves to a generated sentinel declaration, or fall back to Incomplete.

  2. [P2] Aliases become distinct sentinel types — line 953. Given MISSING = sentinel("MISSING"); ALIAS = MISSING, both names get independent sentinel(...) declarations. Consequently, mypy rejects value: MISSING = ALIAS, although they are the same runtime object. Preserve aliases by tracking sentinel identity.

  3. [P2] Keyword names crash generation — line 570. "in".isidentifier() returns true, so a default created with sentinel("in") produces an annotation that raises an uncaught SyntaxError. Reject keywords as well as non-identifiers.


I think these are all pretty marginal, and fixing them would likely be complex. I'm OK shipping this version and making further enhancements later as necessary.

@edgarrmondragon

Copy link
Copy Markdown
Contributor Author

[P2] Sentinel names aren’t necessarily usable type references — lines 570–571 For MISSING = sentinel("missing"), the stub declares MISSING but annotates defaults with Incomplete | missing, producing an undefined-name error.

Yeah, MISSING = sentinel("missing") would be discouraged anyway I think.

[P2] Keyword names crash generation — line 570. "in".isidentifier() returns true, so a default created with sentinel("in") produces an annotation that raises an uncaught SyntaxError. Reject keywords as well as non-identifiers.

Similar to the above, but much easier to address: edgarrmondragon#1.

Class-attribute sentinels also produce invalid type references because their declarations remain ClassVar[sentinel].

Also easy to address: edgarrmondragon#1.

[P2] Aliases become distinct sentinel types — line 953. Given MISSING = sentinel("MISSING"); ALIAS = MISSING, both names get independent sentinel(...) declarations. Consequently, mypy rejects value: MISSING = ALIAS, although they are the same runtime object. Preserve aliases by tracking sentinel identity.

Doable, but I think it'd require stubgen to keep track of all sentinel declarations (by hash maybe?)

@edgarrmondragon edgarrmondragon changed the title Handle arg=<sentinel value> in stubgenc by generating an ellipsis default annotated as Incomplete Handle arg=<sentinel value> in stubgen Sep 27, 2026
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