Skip to content

Instantly share code, notes, and snippets.

@mypy-play
Created May 28, 2025 19:47
Show Gist options
  • Save mypy-play/4b459d87e36b5f45c5076bc41a69896d to your computer and use it in GitHub Desktop.
Save mypy-play/4b459d87e36b5f45c5076bc41a69896d to your computer and use it in GitHub Desktop.
Shared via mypy Playground
# This example is copied verbatim from one of the codebases I contribute to.
# Unfortunately, I can't copy it, so I tried to extract the scenario as it was,
# as an example that I could share publicly.
#
# I'm sure the example could be simplified, but I thought it would be better to
# show it as it is, and leave any conclusions to whomever is going to read it.
#
from typing import Union, TypeVar, Callable
from typing_extensions import TypeAlias, Literal, overload
from dataclasses import dataclass
_T = TypeVar("_T")
_U = TypeVar("_U")
SomeEnumStr: TypeAlias = Union[
Literal["value-a"],
Literal["value-b"],
Literal["value-c"],
]
@dataclass
class MyConfig:
value: SomeEnumStr | None
@overload
def _required(
value: _T | None,
) -> _T | None: ...
@overload
def _required(
value: _T | None,
*,
default: _U,
) -> _T | _U: ...
@overload
def _required(
value: _T | None,
*,
default_factory: Callable[[], _U],
) -> _T | _U: ...
# NOTE: Please ignore the "missing return statements", I just want to show how
# types are deduced later on in the example. Focus here is on the returned
# static types. In the original codebase, all of the functions are implemented.
#
def _required(
value: _T | None,
*,
default: _U | None = None,
default_factory: Callable[[], _U] | None = None,
) -> _T | _U: ...
def ensure_optional_some_enum_str(value: Any, default: _T) -> SomeEnumStr | _T: ...
def get_some_cli_arg() -> str | None: ...
def get_sys_config() -> MyConfig: ...
# Suppose I have a very simple routine, where a configuration can come from
# the CLI (as an argument, parsed somewhere), from a system-dependent location,
# or from a fixed hard-coded value. Precedence is given as CLI, System and
# hardcoded. This `_required` method is used as a helper, to identify when the
# value is not given (in this case, modeled simply as a `None` value).
#
# `ensure_optional_some_enum_str` guarantees that the value provided at the CLI
# respects the ones valid for `SomeEnumStr`. If the CLI's value is `None`, it
# just returns `None`.
#
cli_config = ensure_optional_some_enum_str(get_some_cli_arg(), None)
sys_config = get_sys_config().value
default_config: SomeEnumStr = "value-c"
# `value` is supposed to be the result of the resolution process I described
# earlier. It should have type `SomeEnumStr`.
#
value = _required(
cli_config,
default_factory=lambda: _required(
sys_config,
default=default_config,
),
)
# However, according to PyRight, the type of `value` is just `str`.
#
# My expectation is the same as MyPy. None of the values involved are raw strings.
# All of them are narrowed to `SomeEnumStr`, or to `SomeEnumStr | None`.
# Somewhere in the middle of static checking, some value is being widened to `str`
# for some reason -- and I suppose it's happening at the method parameter type
# checking, either on `default` or `default_factory`.
#
# Perhaps it's the usage of `TypeVar` that's causing this issue? I could force
# a `bound` (e.g. `_T = TypeVar("_T", bound=SomeEnumStr)`), but that would go
# counter to the objective of the TypeVar in the first place, which is to allow
# `_required` to receive a default (or factory's return type) of any type, to
# be used when the value being verified is `None`.
#
reveal_type(value) # ==> [PyLance] Type of "value" is "str"
# ==> note: Revealed type is "Union[Literal['value-a'], Literal['value-b'], Literal['value-c']]"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment