Alright, let’s get our hands dirty. You’ve run mypy on your beautiful, seemingly perfect code, and it has responded by throwing a tantrum fit for a toddler denied candy. Don’t take it personally. mypy isn’t being a jerk; it’s just a deeply pedantic friend who takes your type hints more seriously than you do. Its errors are its way of saying, “I’m trying to protect you from yourself.” Let’s decode its unique brand of angst.

“Incompatible types in assignment”

This is the classic. You’ll see this more often than you see import this. It’s mypy’s fundamental job: catching the moment you try to shove a square peg into a round hole.

# This will make mypy very, very unhappy.
def greet(name: str) -> None:
    print(f"Hello, {name}!")

# All good so far...
greet("Alice")

# And now, the betrayal.
username = 123  # Oh, so it's an integer now, is it?
greet(username)  # Error: Argument 1 to "greet" has incompatible type "int"; expected "str"

The Fix: This is usually straightforward. You either fix the type of the variable you’re assigning (username = "Bob"), or you fix the function signature to accept what you’re actually giving it (def greet(name: str | int) -> None). The choice depends on what your code’s intent should be. mypy is forcing you to make that decision explicitly.

“Item X of Y has no type Z”

This one is a rite of passage. You create a list, you put some stuff in it, and mypy acts like you’ve summoned a demon. Why? Because it can’t always infer the type of an empty container.

# You, an optimist:
my_list = []  # "It's a list! Obviously!"
my_list.append("a string")

# mypy, a pessimist:
# "You told me `my_list` is a list. A list of WHAT, exactly? I'm not a mind reader."
# Error: Need type annotation for "my_list" (hint: "my_list: list[<type>] = []")

The Fix: Annotate the empty collection explicitly. It feels a bit verbose, but it removes all ambiguity. This is mypy asking for a firm commitment.

# The correct, unambiguous way:
my_list: list[str] = []  # Ah, a list of strings. Crystal clear.
my_list.append("a string")  # mypy nods in approval.

# Or, if you're a fan of later Python versions:
my_list: list[str] = []

“Function does not return a value”

You wrote a function that returns a value in an if block, but mypy is complaining it might not return anything. This is mypy being your overzealous financial auditor—it needs to account for every single code path.

def get_rating(score: int) -> str:
    if score > 90:
        return "A"
    elif score > 70:
        return "B"
    # What if score is 50? The function just... ends?
    # Error: Missing return statement

The Fix: You must handle the fall-through case. Either return a default value or raise an exception. Never leave the return type -> str with a potential to return None silently. That’s how runtime NoneType errors are born.

def get_rating(score: int) -> str:
    if score > 90:
        return "A"
    elif score > 70:
        return "B"
    else:
        return "C"  # All code paths are now accounted for. The auditor is satisfied.

# Or, if a low score is an error condition, be explicit about it!
def get_rating_strict(score: int) -> str:
    if score > 90:
        return "A"
    elif score > 70:
        return "B"
    raise ValueError("Score is too low to be rated")

Dealing with None and Optional Values

This is where mypy earns its keep. The billion-dollar mistake. You have a variable that might be None, and mypy catches you trying to use it as if it’s definitely not None.

def print_length(maybe_string: str | None) -> None:
    # Danger! What if maybe_string is None?
    print(len(maybe_string))  # Error: Object of type "None" has no attribute "__len__"

The Fix: You must perform a guard check. This forces you to write safer, more defensive code. mypy is smart enough to understand that after an if check, the type within that block is narrowed.

def print_length_safe(maybe_string: str | None) -> None:
    if maybe_string is None:
        print("Got nothing!")
    else:
        # mypy knows that within this branch, `maybe_string` is absolutely a `str`.
        print(len(maybe_string))  # All good!

The Library Stumbling Block: Stubs and Any

Sometimes, you’ll use a library that doesn’t have type hints. mypy sees this and basically throws its hands up in the air, treating everything from that library as Any—a magical type that turns off all type checking. This is a common source of “mystery errors” where the problem isn’t in your code, but in the lack of information.

import some_legacy_library_with_no_types

result = some_legacy_library_with_no_types.get_data()  # type: Any
result.this_method_doesnt_exist()  # mypy: "I see no problem here" *sips coffee*
# ...Runtime: AttributeError

The Fix: This is a rough edge in the ecosystem. Your best weapons are:

  1. Use libraries that have type stubs (package-name-stubs on PyPI) or inline types.
  2. Write your own stubs if you have to (a advanced but powerful move).
  3. Use explicit annotations immediately after calling the library to pin down the type you expect.
import some_legacy_library_with_no_types

# Tell mypy what you expect 'result' to be, because the library won't.
result: dict[str, int] = some_legacy_library_with_no_types.get_data()
# Now mypy can check if `result` is used as a dict correctly.

The key takeaway? mypy’s errors aren’t obstacles; they’re conversations. They’re forcing you to clarify your intent, close off edge cases, and write code that’s robust not just by accident, but by design. It’s a pain until suddenly it isn’t, and you find yourself wondering how you ever coded without it.