Skip to content

lookup_default returns Sentinel.UNSET instead of None #3145

Description

@peterlynch

Upgrading click to 8.3.0 or 8.3.1 causes a regression in lookup_default

Reproduce

Change the click dependency in the below reproduce case to 8.2.1 and the assertion does not fail.

Install uv. Run with uv run repro_click_unset_regression.py or simply `repro_click_unset_regression.py'.

#!/usr/bin/env -S uv run
# /// script
# dependencies = ["click==8.3.1"]
# ///
"""
Run with uv installed:
  repro_click_unset_regression.py
Expected (pre 8.3.0): lookup_default returns the prefix-level default string or None.
Actual (8.3.0+): returns Sentinel.UNSET instead of None.
"""
import importlib.metadata
import sys
import click

class CustomClickContext(click.Context):
    def lookup_default(self, name: str, call: bool = False):
        if call:
            default = super().lookup_default(name, call=True)  # call: Literal[True]
        else:
            default = super().lookup_default(name, call=False)  # call: Literal[False]

        # Original logic (kept to reproduce): treats any non-None (including sentinel) as a real default.
        if default is not None:
            return default
        prefix = name.split("_", 1)[0]
        group = getattr(self, "default_map", {}).get(prefix)
        if group:
            return group.get(name)
        return default

def describe(value):
    cls = value.__class__
    return {
        "repr": repr(value),
        "type": f"{cls.__module__}.{cls.__qualname__}",
        "has_name_attr": hasattr(value, "name"),
        "name_attr": getattr(value, "name", None),
    }

def main():
    default_map = {"app": {"email": "prefix-param-level@example.com"}}
    cmd = click.Command("get-views")
    ctx = CustomClickContext(cmd, info_name=None)
    ctx.default_map = default_map

    val = ctx.lookup_default("email", False)
    info = describe(val)
    print(f"click version (importlib.metadata): {importlib.metadata.version('click')}")
    print("lookup_default raw:", info)
    # This assert fails under ≥ 8.3.0 because val is the internal Sentinel.UNSET object.
    assert val is None, (
        f"Regression: got {info['repr']} ({info['type']}) instead of expected builtins.NoneType"
    )

if __name__ == "__main__":
    try:
        main()
    except AssertionError as e:
        print("AssertionError:", e)
        sys.exit(1)

Expected

Return None instead of internal Sentinel.UNSET detail.

Environment:

  • Python version: 3.14
  • Click version: 8.30, 8.3.1

Activity

  1. davidism commented on Nov 17, 2025

    @davidism
    Member

    @kdeldycke it's time to just make a new API. The problem will be that people are overriding the old API still, so how do we account for that. Maybe we just make UNSET public after all. It's too hard to account for people overriding unexpected "documented" APIs.

  2. kdeldycke commented on Nov 18, 2025

    @kdeldycke
    Collaborator

    😭

    I guess I'll have to fix it in the same spirit as of the last #3137. Let's see where my investigations leads me.

    @kdeldycke it's time to just make a new API. The problem will be that people are overriding the old API still, so how do we account for that. Maybe we just make UNSET public after all. It's too hard to account for people overriding unexpected "documented" APIs.

    As for the next few weeks/months, we can still play whack-a-mole to stabilize everything in 8.x.y releases. Then just bump Click to 9.0.0, to change the expectations (private/public stuff) and the data contract (UNSET, native Python types/strings, etc). I think it's time to start planning for 9.0.0 with more granular tasks.

  3. changed the title [-]lookup_default returns Sentinel.UNSET instead of None[/-] [+]`lookup_default` returns `Sentinel.UNSET` instead of `None`[/+] on Nov 18, 2025
  4. added this to the 8.3.2 milestone on Nov 18, 2025
  5. added theissue type on Nov 18, 2025
  6. added a commit that references this issue on Jan 31, 2026
    ed2094b
  7. added a commit that references this issue on Feb 17, 2026
    b1e8a07
  8. added a commit that references this issue on Feb 17, 2026
    fe043ee
  9. added 2 commits that reference this issue on Feb 20, 2026
    fe2f919
    2112621
  10. added a commit that references this issue on Feb 20, 2026
    aaf99a3
  11. kdeldycke commented on Feb 20, 2026

    @kdeldycke
    Collaborator

    I have a fix available for review/merge at: #3224

  12. kdeldycke commented on Feb 28, 2026

    @kdeldycke
    Collaborator

    A fix has been merged upstream in stable branch. It will be part of the soon-to-be-released v8.3.2 of Click.

  13. locked as resolved and limited conversation to collaborators on Mar 15, 2026
  14. added
    parsingParsing, parameters, commands, chaining, context
    and removed on Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    parsingParsing, parameters, commands, chaining, context

    Type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions