Skip to content

Change UnsetValueType to single-value enum for better type narrowing#147

Merged
binaryDiv merged 1 commit into
mainfrom
improve-unsetvalue-type-narrowing
May 6, 2026
Merged

Change UnsetValueType to single-value enum for better type narrowing#147
binaryDiv merged 1 commit into
mainfrom
improve-unsetvalue-type-narrowing

Conversation

@binaryDiv
Copy link
Copy Markdown
Contributor

This PR changes the UnsetValueType class from a regular class to a single-value enum class (and therefore changes UnsetValue to be the only member of this enum). This improves type narrowing.

Explanation

The current approach of how the UnsetValue sentinel object is implemented has several drawbacks. The biggest issue for users is that type checkers can't do proper type narrowing on it.

For example, given a variable foo of type str | UnsetValueType, the following 3 if conditions all work to filter out UnsetValue:

foo: str | UnsetValueType

# Identity check
if foo is not UnsetValue:
    assert isinstance(foo, str)  # foo is always str

# Equality check
if foo != UnsetValue:
    assert isinstance(foo, str)  # foo is always str

# Boolean evaluation
if foo:
    assert isinstance(foo, str)  # foo is always str
    assert len(foo) > 0          # additionally, foo is not an empty string

A type checker should be able to recognize this and narrow down the type: Within each of the if blocks, the variable should be treated as a str.

However, with the current approach, this does not work: The identity check is not a type check, it only tells us that var is not the specific object UnsetValue, but from mypy's perspective, it could still be a different object of the type UnsetValueType. There's no way for mypy to know that there is only (and can only be) a single instance of the type UnsetValueType.

This PR replaces the current approach with a different one: Defining UnsetValueType as an enum class that only has a single value UnsetValueType.UnsetValue (and defining UnsetValue as a short name for the enum member).

This solves the problem, because mypy has some special logic for single-value enums. An enum with only a single value is essentially a singleton class, so mypy knows that UnsetValue is the only instance of UnsetValueType. If a variable is not UnsetValue, mypy can infer that the variable cannot be of type UnsetValueType either, so type narrowing works as intended.

Additionally, to allow type narrowing in boolean evaluation (the third example), the return type annotation of UnsetValueType.__bool__ was changed from bool to Literal[False], because the class always evaluates as False.

List of changes

  • UnsetValueType is now an Enum class with the single value UnsetValueType.UnsetValue.
  • UnsetValue is now an alias for UnsetValueType.UnsetValue.
  • The return type of UnsetValueType.__bool__ is now Literal[False] (instead of bool).
  • Minor breaking change: UnsetValueType() now raises an exception instead of returning UnsetValue. This is a side effect of the enum implementation, shouldn't affect anyone in practice though.

@binaryDiv binaryDiv self-assigned this Apr 29, 2026
@binaryDiv binaryDiv merged commit cc4e259 into main May 6, 2026
6 checks passed
@binaryDiv binaryDiv deleted the improve-unsetvalue-type-narrowing branch May 6, 2026 09:54
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.

2 participants