Type Mismatch Errors Hide One Word in 800 Characters: Diff the Two Types Line by Line Instead of Reading Them

That whole message is 817 characters long and the mismatch is one word: List where the method promised IEnumerable. Your eye can't find it because both halves share the same 40 tokens around it. A diff can, and that is the better way: split each type into one token per line, indent by nesting depth, and let a diff tool mark the lines that changed. Below is a 70-line script that does it for C#, Python, TypeScript, Java and Kotlin types, run on all three errors you sent, plus the fix for each one.

Why the IDE Tooltip Can’t Help You Here

Rider and PyCharm print the two types as flat strings, because that’s what the compiler hands them. The quick-fix under the error (“Make ‘_to_numpy’ return …”) only offers to change the annotation to whatever the code happens to produce, which papers over the bug half the time. Three things make the strings unreadable:

  • Namespaces. System.Collections.Generic.IEnumerable is 36 characters for a 11-character idea. C# tooltips always print them in full.
  • Repeated sub-types. A union like dict[Any, Any] | ndarray[Any, Any] | list[ndarray[Any, Any]] shows up in both the expected and actual type, so your eye matches the wrong pair.
  • Nesting without line breaks. A tuple inside a list inside a dictionary is three levels deep, written on one line.

JetBrains does have one built-in route: copy the expected type, open a scratch file (Ctrl+Alt+Shift+Insert), paste the actual type, select it, and use Edit → Compare with Clipboard. It works, but it diffs characters on one line, so you still get a long red smear. The script below diffs tokens instead.

A 70-Line Script That Marks the Mismatch

Save this as typediff.py anywhere on your PATH. It strips namespaces, drops C# tuple element names (FormationData Formation becomes FormationData), breaks each type into one token per line, and uses Python’s own difflib to line the two up.

"""typediff.py - show where two type signatures differ.

Usage:
    python typediff.py "EXPECTED TYPE" "ACTUAL TYPE"
    python typediff.py            (then paste the two types, one per line)

Works on C#, TypeScript, Python, Java, Kotlin and Rust type strings,
because all of them nest with < > [ ] ( ) and separate with , and |.
"""
import re
import sys

OPEN, CLOSE = "<[(", ">])"


def strip_namespaces(s: str) -> str:
    # System.Collections.Generic.List -> List, LeoImportAPI.FBS.Foo -> Foo
    return re.sub(r"\b(?:[A-Za-z_]\w*\.)+(?=[A-Za-z_]\w*)", "", s)


def tokens(s: str) -> list[str]:
    s = strip_namespaces(s).replace("'", "")
    raw = re.findall(r"[<>\[\](),|]|[^<>\[\](),|\s]+", s)
    out: list[str] = []
    for t in raw:
        # "FormationData Formation" is a C# named tuple element: keep the type, drop the label
        if out and out[-1] not in "<[(>]),|" and t not in "<[(>]),|":
            continue
        out.append(t)
    return out


def outline(toks: list[str]) -> list[str]:
    """One token per line, indented by nesting depth."""
    lines, depth = [], 0
    for t in toks:
        if t in CLOSE:
            depth -= 1
        lines.append("  " * depth + t)
        if t in OPEN:
            depth += 1
    return lines


def diff(a: list[str], b: list[str]) -> list[tuple[str, str, str]]:
    """Line up the two outlines and mark each line: ' ' same, '-' only expected, '+' only actual."""
    import difflib
    out = []
    for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a, b, autojunk=False).get_opcodes():
        if tag == "equal":
            out += [(" ", x, x) for x in a[i1:i2]]
        else:
            left, right = a[i1:i2], b[j1:j2]
            for k in range(max(len(left), len(right))):
                out.append(("!", left[k] if k < len(left) else "", right[k] if k < len(right) else ""))
    return out


def main() -> None:
    if len(sys.argv) >= 3:
        expected, actual = sys.argv[1], sys.argv[2]
    else:
        print("Paste the EXPECTED type, press Enter, paste the ACTUAL type, press Enter:")
        expected, actual = input().strip(), input().strip()

    a, b = outline(tokens(expected)), outline(tokens(actual))
    width = max(len(x) for x in a + b) + 2
    print(f"\n{'EXPECTED':<{width}}  ACTUAL")
    print("-" * (width * 2 + 2))
    mismatches = 0
    for mark, left, right in diff(a, b):
        flag = "   " if mark == " " else ">> "
        if mark != " ":
            mismatches += 1
        print(f"{flag}{left:<{width}}{right}")
    print(f"\n{mismatches} line(s) differ. Lines starting with >> are the mismatch.")


if __name__ == "__main__":
    main()

Run it with the two types as arguments, or run it with no arguments and paste them when asked. Quotes from the tooltip don’t matter, the script removes them.

Error 1: C# Says List Where the Method Promised IEnumerable

python typediff.py "System.Collections.Generic.Dictionary<System.DateTime,System.Collections.Generic.IEnumerable<(...)>>" "System.Collections.Generic.Dictionary<System.DateTime,System.Collections.Generic.List<(...)>>"
What the script printed (shortened)
EXPECTED ACTUAL
------------------------------------------------------------
Dictionary Dictionary
< <
DateTime DateTime
, ,
>> IEnumerable List
< <
( (
TrainpartSequenceInfo TrainpartSequenceInfo
... ...

1 line(s) differ. Lines starting with >> are the mismatch.

One line differs. The reason the compiler won’t accept it: Dictionary<K, V> is not covariant, so a Dictionary<DateTime, List<T>> can never be passed off as a Dictionary<DateTime, IEnumerable<T>>, even though a List<T> on its own converts to IEnumerable<T> fine. You have two clean fixes.

return byDay.ToDictionary(
    kv => kv.Key,
    kv => (IEnumerable<(TrainpartSequenceInfo SequenceInfo,
                        FormationData Formation,
                        IEnumerable<(TrainpartInfo, FormationInfo)> TpList,
                        IEnumerable<DateTime> OppDates)>)kv.Value);
var byDay = new Dictionary<DateTime, IEnumerable<(TrainpartSequenceInfo SequenceInfo,
                                                 FormationData Formation,
                                                 IEnumerable<(TrainpartInfo, FormationInfo)> TpList,
                                                 IEnumerable<DateTime> OppDates)>>();
// ... fill it with lists as before; a List<T> assigns to IEnumerable<T> ...
return byDay;

Fix B is the one to pick if nothing after this method needs .Add() on the values. The tuple element names (SequenceInfo, Formation) were never part of the problem. C# ignores them when it compares tuple types, and the script drops them for the same reason.

Error 2: Python Builds a tuple[int, …] | None and the Annotation Says tuple[int]

python typediff.py "tuple[tuple[int], tuple[int], int, MOSyncVectorEnv, ConflictEnv]" "tuple[tuple[int, ...] | None, tuple[Any], int, MOSyncVectorEnv, ConflictEnv]"
What the script printed
EXPECTED ACTUAL
----------------------------------------
tuple tuple
[ [
tuple tuple
[ [
int int
>> ,
>> ...
] ]
>> |
>> None
, ,
tuple tuple
[ [
>> int Any
] ]
, ,
int int
... ...

5 line(s) differ. Lines starting with >> are the mismatch.

Two separate problems show up, and the diff keeps them apart:

  • Position 1: the annotation says tuple[int], which in Python typing means a tuple of exactly one int. The code returns a tuple of any length, and it can be None, most likely from env.observation_space.shape, which Gymnasium types as tuple[int, ...] | None.
  • Position 2: tuple[Any] means the checker lost track of what went in, usually because it came from an unannotated space object.
def generate_envs(...) -> tuple[tuple[int, ...], tuple[int, ...], int, MOSyncVectorEnv, ConflictEnv]:
    env_temp = ConflictEnv(...)
    assert isinstance(env_temp.action_space, spaces.Discrete)

    obs_shape = env_temp.observation_space.shape
    assert obs_shape is not None, "observation space has no shape"

    reward_shape: tuple[int, ...] = tuple(int(n) for n in env_temp.reward_space.shape or ())
    n_actions = int(env_temp.action_space.n)

    return obs_shape, reward_shape, n_actions, vector_env, env_temp

The assert is what removes None from the type, so the checker is happy and you get a clear failure message if a space ever has no shape.

Error 3: Python Loses the Key Type Because the Dict Was Matched Without One

python typediff.py "dict[str, dict[Any, Any] | ndarray[Any, Any] | list[ndarray[Any, Any]] | ndarray[Any, Any] | int]" "dict[Any, dict[str, dict[Any, Any] | ndarray[Any, Any]] | list[ndarray[Any, Any]] | ndarray[Any, Any] | int]"
What the script printed (shortened)
EXPECTED ACTUAL
----------------------------
dict dict
[ [
>> str Any
, ,
dict dict
[ [
>> Any str
, ,
>> Any dict
>> ] [
... ...

Read the first >> line and stop there. case dict(): narrows obj to a bare dict, so key in obj.items() is Any, and the comprehension you return inherits that. The later lines are the same problem one level down: the function calls itself, so the nested dict’s type is whatever the recursion returned. Fix both with one recursive alias and a str() on the key.

from typing import Any, TypeAlias
import numpy as np
from numpy.typing import NDArray

NumpyTree: TypeAlias = "dict[str, NumpyTree] | NDArray[Any] | list[NDArray[Any]] | int"

def _to_numpy(self, obj: Any, key_name: str = "", reward_type: str = "") -> NumpyTree:
    match obj:
        case dict():
            return {{
                str(key): self._to_numpy(value, key_name=f"{{key_name}}.{{key}}", reward_type=reward_type)
                for key, value in obj.items()
            }}
        case list():
            return [np.asarray(v) for v in obj]
        case int():
            return obj
        case _:
            return np.asarray(obj)

Where Your Compiler Already Does This for You

  • Rust prints expected `X`, found `Y` on two lines and, since 1.70, highlights the differing segment in colour. Nothing to add.
  • TypeScript: tsc --pretty colours the message, and from 4.x it shortens repeated sub-types with .... For a long one, copy both types from the message into the script above. The tokenizer handles < >, [ ] and |.
  • mypy: mypy --pretty wraps long messages. Add reveal_type(byDay) in the code to print the actual type on its own line.
  • C#: paste the full type into a using Alias = ...; line at the top of the file. The compiler then uses the short alias in every error, and the diff gets 400 characters shorter before you start.

Related blog posts