From 22307d944dfa030e4557a2e5f778605ba59f2b18 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 21 Feb 2026 01:09:22 +0000 Subject: [PATCH 1/3] Add flag to reveal simpler types --- mypy/main.py | 6 ++++++ mypy/messages.py | 2 ++ mypy/options.py | 1 + mypy/types.py | 38 +++++++++++++++++++++++++++------ test-data/unit/check-flags.test | 16 ++++++++++++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/mypy/main.py b/mypy/main.py index acc27f1806b78..c469715fc401b 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -989,6 +989,12 @@ def add_invertible_flag( help="Show links to error code documentation", group=error_group, ) + add_invertible_flag( + "--reveal-simple-types", + default=False, + help="Use compact (but potentially ambiguous) type representation in reveal_type()", + group=error_group, + ) add_invertible_flag( "--pretty", default=False, diff --git a/mypy/messages.py b/mypy/messages.py index 59b1b3db07cc8..ff2388802fb1a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1744,6 +1744,7 @@ def reveal_type(self, typ: Type, context: Context) -> None: # Nothing special here; just create the note: visitor = TypeStrVisitor(options=self.options) + visitor.reveal_simple_types = self.options.reveal_simple_types self.note(f'Revealed type is "{typ.accept(visitor)}"', context) def reveal_locals(self, type_map: dict[str, Type | None], context: Context) -> None: @@ -1754,6 +1755,7 @@ def reveal_locals(self, type_map: dict[str, Type | None], context: Context) -> N self.note("Revealed local types are:", context) for k, v in sorted_locals.items(): visitor = TypeStrVisitor(options=self.options) + visitor.reveal_simple_types = self.options.reveal_simple_types self.note(f" {k}: {v.accept(visitor) if v is not None else None}", context) else: self.note("There are no locals to reveal", context) diff --git a/mypy/options.py b/mypy/options.py index 3dc72c7e30512..fe8090d284528 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -371,6 +371,7 @@ def __init__(self) -> None: self.show_error_end: bool = False self.hide_error_codes = False self.show_error_code_links = False + self.reveal_simple_types = False # Use soft word wrap and show trimmed source snippets with error location markers. self.pretty = False self.dump_graph = False diff --git a/mypy/types.py b/mypy/types.py index db8fd4605659c..bc4f288f6edfe 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3719,6 +3719,9 @@ def __init__(self, id_mapper: IdMapper | None = None, *, options: Options) -> No self.id_mapper = id_mapper self.options = options self.dotted_aliases: set[TypeAliasType] | None = None + # This visitor is used in other contexts (not just reveal_type()), so only + # use this option when explicitly set by the caller. + self.reveal_simple_types = False def visit_unbound_type(self, t: UnboundType, /) -> str: s = t.name + "?" @@ -3759,6 +3762,8 @@ def visit_instance(self, t: Instance, /) -> str: # Instances with a literal fallback should never be generic. If they are, # something went wrong so we fall back to showing the full Instance repr. s = f"{t.last_known_value.accept(self)}?" + elif self.reveal_simple_types: + s = t.type.name or "" else: s = t.type.fullname or t.type.name or "" @@ -3775,10 +3780,13 @@ def visit_instance(self, t: Instance, /) -> str: return s def visit_type_var(self, t: TypeVarType, /) -> str: - s = f"{t.name}`{t.id}" + if self.reveal_simple_types: + s = t.name + else: + s = f"{t.name}`{t.id}" if self.id_mapper and t.upper_bound: s += f"(upper_bound={t.upper_bound.accept(self)})" - if t.has_default(): + if t.has_default() and not self.reveal_simple_types: s += f" = {t.default.accept(self)}" return s @@ -3787,7 +3795,10 @@ def visit_param_spec(self, t: ParamSpecType, /) -> str: s = "" if t.prefix.arg_types: s += f"[{self.list_str(t.prefix.arg_types)}, **" - s += f"{t.name_with_suffix()}`{t.id}" + if self.reveal_simple_types: + s += t.name_with_suffix() + else: + s += f"{t.name_with_suffix()}`{t.id}" if t.prefix.arg_types: s += "]" if t.has_default(): @@ -3824,8 +3835,11 @@ def visit_parameters(self, t: Parameters, /) -> str: return f"[{s}]" def visit_type_var_tuple(self, t: TypeVarTupleType, /) -> str: - s = f"{t.name}`{t.id}" - if t.has_default(): + if self.reveal_simple_types: + s = t.name + else: + s = f"{t.name}`{t.id}" + if t.has_default() and not self.reveal_simple_types: s += f" = {t.default.accept(self)}" return s @@ -3854,7 +3868,10 @@ def visit_callable_type(self, t: CallableType, /) -> str: s += name + ": " type_str = t.arg_types[i].accept(self) if t.arg_kinds[i] == ARG_STAR2 and t.unpack_kwargs: - type_str = f"Unpack[{type_str}]" + if self.reveal_simple_types: + type_str = f"**{type_str}" + else: + type_str = f"Unpack[{type_str}]" s += type_str if t.arg_kinds[i].is_optional(): s += " =" @@ -3933,7 +3950,10 @@ def item_str(name: str, typ: str) -> str: prefix = "" if t.fallback and t.fallback.type: if t.fallback.type.fullname not in TPDICT_FB_NAMES: - prefix = repr(t.fallback.type.fullname) + ", " + if self.reveal_simple_types: + prefix = t.fallback.type.name + ", " + else: + prefix = repr(t.fallback.type.fullname) + ", " return f"TypedDict({prefix}{s})" def visit_raw_expression_type(self, t: RawExpressionType, /) -> str: @@ -3967,6 +3987,8 @@ def visit_placeholder_type(self, t: PlaceholderType, /) -> str: def visit_type_alias_type(self, t: TypeAliasType, /) -> str: if t.alias is None: return "" + if self.reveal_simple_types: + return t.alias.name if not t.is_recursive: return get_proper_type(t).accept(self) if self.dotted_aliases is None: @@ -3979,6 +4001,8 @@ def visit_type_alias_type(self, t: TypeAliasType, /) -> str: return type_str def visit_unpack_type(self, t: UnpackType, /) -> str: + if self.reveal_simple_types: + return f"*{t.type.accept(self)}" return f"Unpack[{t.type.accept(self)}]" def list_str(self, a: Iterable[Type], *, use_or_syntax: bool = False) -> str: diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 5e9c7f4be8dcd..b8ed8dbfecf55 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2737,3 +2737,19 @@ def f() -> None: [file mypy.ini] \[mypy] disallow_redefinition = true + +[case testRevealSimpleTypes] +# flags: --reveal-simple-types +from typing_extensions import TypeVarTuple, Unpack, TypedDict + +Ts = TypeVarTuple("Ts") +def foo(x: int, *xs: Unpack[Ts]) -> tuple[int, Unpack[Ts]]: ... +reveal_type(foo) # N: Revealed type is "def [Ts] (x: int, *xs: *Ts) -> tuple[int, *Ts]" + +class Options(TypedDict): + rate: int + template: str + +def bar(x: int, **kwargs: Unpack[Options]) -> None: ... +reveal_type(bar) # N: Revealed type is "def (x: int, **kwargs: **TypedDict(Options, {'rate': int, 'template': str}))" +[builtins fixtures/tuple.pyi] From a0b377309454682160d0e2e1a9eedd99d3b0ac8d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 21 Feb 2026 01:18:57 +0000 Subject: [PATCH 2/3] Better test --- test-data/unit/check-flags.test | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index b8ed8dbfecf55..407d1081732d0 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2740,16 +2740,23 @@ disallow_redefinition = true [case testRevealSimpleTypes] # flags: --reveal-simple-types +from typing import Generic, TypeVar from typing_extensions import TypeVarTuple, Unpack, TypedDict +T = TypeVar("T", bound=int) +S = TypeVar("S", default=list[T]) + +class A(Generic[T, S]): ... +reveal_type(A) # N: Revealed type is "def [T <: int, S = list[T]] () -> A[T, S]" + Ts = TypeVarTuple("Ts") def foo(x: int, *xs: Unpack[Ts]) -> tuple[int, Unpack[Ts]]: ... reveal_type(foo) # N: Revealed type is "def [Ts] (x: int, *xs: *Ts) -> tuple[int, *Ts]" -class Options(TypedDict): - rate: int - template: str +class User(TypedDict): + age: int + name: str -def bar(x: int, **kwargs: Unpack[Options]) -> None: ... -reveal_type(bar) # N: Revealed type is "def (x: int, **kwargs: **TypedDict(Options, {'rate': int, 'template': str}))" +def bar(x: int, **kwargs: Unpack[User]) -> None: ... +reveal_type(bar) # N: Revealed type is "def (x: int, **kwargs: **TypedDict(User, {'age': int, 'name': str}))" [builtins fixtures/tuple.pyi] From eda291372dd23a0742f196ab2d064f6f0748ed0e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 21 Feb 2026 01:44:55 +0000 Subject: [PATCH 3/3] Better handling for generic aliases --- mypy/types.py | 5 ++++- test-data/unit/check-flags.test | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mypy/types.py b/mypy/types.py index bc4f288f6edfe..e687ecd287952 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3988,7 +3988,10 @@ def visit_type_alias_type(self, t: TypeAliasType, /) -> str: if t.alias is None: return "" if self.reveal_simple_types: - return t.alias.name + type_str = t.alias.name + if t.args: + type_str += f"[{self.list_str(t.args)}]" + return type_str if not t.is_recursive: return get_proper_type(t).accept(self) if self.dotted_aliases is None: diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 407d1081732d0..6660a828bceb2 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2759,4 +2759,8 @@ class User(TypedDict): def bar(x: int, **kwargs: Unpack[User]) -> None: ... reveal_type(bar) # N: Revealed type is "def (x: int, **kwargs: **TypedDict(User, {'age': int, 'name': str}))" + +Alias = tuple[T, S] +x: Alias[int, str] +reveal_type(x) # N: Revealed type is "Alias[int, str]" [builtins fixtures/tuple.pyi]