Skip to main content

Common Type Hint Errors: Debugging mypy Messages

mypy's error messages are precise and actionable, but their wording can feel cryptic to beginners. This article teaches you how to read mypy's output, understand what went wrong, and fix the most common type-checking issues. By the end, you'll be able to interpret any mypy error and correct it quickly.

Understanding mypy Error Format

Every mypy error follows a consistent structure. Here's an example:

hello.py:5: error: Argument 1 to "greet" has incompatible type "int"; expected "str"

Break this down:

  • hello.py:5 — File name and line number where the error occurred
  • error — Severity level (mypy also uses note and warning)
  • Argument 1 to "greet" — What mypy is complaining about
  • has incompatible type "int"; expected "str" — What the problem is

When you see an error, look at three things: (1) the file and line number, (2) what part of the code mypy is examining, and (3) what type mismatch occurred. This pattern repeats for nearly all mypy errors.

Error Type 1: Incompatible Types (Most Common)

This error occurs when you pass or assign a value of the wrong type:

def add(a: int, b: int) -> int:
return a + b

# Line 5: incompatible type
result: str = add(5, 3) # mypy: error: Incompatible types in assignment (expression has type "int", variable has type "str")

The problem: add() returns an int, but you're trying to assign it to a str variable. Fix this by changing the annotation or the assignment:

# Fix 1: Change the variable annotation
result: int = add(5, 3)

# Fix 2: Convert the value (less common)
result_str: str = str(add(5, 3))

Another common case: passing the wrong type to a function:

def greet(name: str) -> str:
return f"Hello, {name}!"

# Passing an int where str is expected
message = greet(42) # mypy: error: Argument 1 to "greet" has incompatible type "int"; expected "str"

Fix by passing the correct type:

message = greet("Alice")  # Correct

Or cast the argument (less preferred, as it silences mypy without fixing the real problem):

message = greet(str(42))  # Works, but is 42 really supposed to be stringified?

Error Type 2: Missing Type Annotations in Strict Mode

In strict mode, every function parameter and return type must be explicitly annotated. Omitting either causes an error:

def add(a, b):  # mypy: error: Function is missing a type annotation for one or more arguments
return a + b

def greet(name: str): # mypy: error: Function is missing a return type annotation
return f"Hello, {name}!"

Fix by adding annotations to all parameters and return types:

def add(a: int, b: int) -> int:
return a + b

def greet(name: str) -> str:
return f"Hello, {name}!"

If you're not in strict mode but see error: Function is missing a type annotation for one or more arguments, you likely enabled disallow_untyped_defs in your mypy config. The fix is the same: annotate all parameters and returns.

Error Type 3: Possible None and NoneType Errors

mypy is strict about None. If a variable could be None, you must handle it:

def get_age(user_id: int) -> int | None:
# Simulate a lookup that might fail
if user_id == 1:
return 25
return None

age = get_age(1)
print(age + 5) # mypy: error: Unsupported operand type(s) for +: "int" and "None"

The problem: age could be None, and you cannot add None to an int. Fix by checking for None first:

age = get_age(1)
if age is not None:
print(age + 5) # Safe: age is definitely an int here

Or use a guard clause at the start of the function:

def display_age(user_id: int) -> None:
age = get_age(user_id)
if age is None:
print("Age not found")
return
print(age + 5) # Safe: age is int, not None

mypy understands if x is not None: and if x is None: and narrows the type inside the conditional. This is called type narrowing.

Error Type 4: Accessing Attributes That Don't Exist

If a type doesn't have an attribute or method, mypy reports it:

name: str = "Alice"
print(name.upper()) # OK: str has an upper() method
print(name.reverse()) # mypy: error: "str" has no attribute "reverse"

The problem: strings have no reverse() method (only lists do). Fix by using the correct method or checking the API:

name: str = "Alice"
print(name[::-1]) # Correct: slice reversal

A more subtle case with dictionaries:

from typing import Dict

config: Dict[str, int] = {"timeout": 30}
print(config["timeout"]) # OK: key exists
print(config.get("retries")) # OK: get() returns None if key missing
print(config["retries"]) # mypy: error: Key "retries" not in Dict

If you're accessing a key that might not exist, use .get() instead of bracket notation:

retries = config.get("retries", 0)  # Returns 0 if "retries" not present

Error Type 5: Incorrect Generic Type Parameters

Generics like List[int] must have compatible items:

from typing import List

numbers: List[int] = [1, 2, 3]
numbers.append("4") # mypy: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"

The annotation says the list holds integers, but you're appending a string. Fix by appending the correct type:

numbers: List[int] = [1, 2, 3]
numbers.append(4) # Correct

Or change the annotation if strings are actually allowed:

from typing import List, Union

mixed: List[Union[int, str]] = [1, "2", 3]
mixed.append("4") # OK: strings are allowed

Error Type 6: Type Narrowing and Assumption Failures

mypy is conservative: it won't assume a variable has changed type unless you explicitly check:

from typing import Optional

def show_item(items: list, index: int) -> None:
if index < len(items):
item = items[index]
# mypy assumes item could be Any (unknown type)
print(item.upper()) # mypy: error: Need to cast item for upper() to be available (if not Any)

Fix by explicitly typing the list contents:

from typing import List

def show_item(items: List[str], index: int) -> None:
if index < len(items):
item = items[index] # mypy now knows item is str
print(item.upper()) # OK

Error Type 7: Callable Types and Function Signatures

If you pass a function to another function, both signatures must be compatible:

from typing import Callable

def apply_operation(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)

def add(x: int, y: int) -> int:
return x + y

result = apply_operation(add, 5, 3) # OK

def multiply(x: float, y: float) -> float:
return x * y

result = apply_operation(multiply, 5, 3) # mypy: error: Argument 1 has incompatible type Function

The problem: multiply takes float but apply_operation expects a function that takes int. Fix by using a function with the correct signature, or by making apply_operation more flexible:

from typing import Callable, TypeVar

T = TypeVar("T", bound=int | float)

def apply_operation(func: Callable[[T, T], T], a: T, b: T) -> T:
return func(a, b)

result = apply_operation(multiply, 5.0, 3.0) # OK

Practical Debugging Workflow

When mypy reports an error, follow these steps:

  1. Read the error line number — Go directly to that line in your editor.
  2. Understand what mypy is examining — Is it a function call? An assignment? A method access?
  3. Identify the type mismatch — What type does mypy expect vs. what did you provide?
  4. Check the variable annotations — Are they correct? Should they be Optional?
  5. Run mypy again — Verify the fix worked. If new errors appear, repeat from step 1.

Example debugging session:

# mypy reports: line 5: error: Argument 1 to "greet" has incompatible type "int"; expected "str"

def greet(name: str) -> str:
return f"Hello, {name}!"

result = greet(42) # Line 5

# Step 1 & 2: Line 5 is a function call to greet()
# Step 3: greet expects a str, but I passed an int
# Step 4: The annotation is correct (greet should take str)
# Step 5: Change the argument
result = greet(str(42)) # or greet("42")

Key Takeaways

  • mypy errors follow a consistent format: file:line: error: [part] [issue]
  • Incompatible type errors mean you assigned or passed the wrong type—fix by changing the value or annotation
  • In strict mode, all function parameters and returns must be annotated
  • None is a distinct type—always check for None before using a nullable value
  • Use type narrowing (if statements) to help mypy understand type changes
  • Generics like List[int] are strict about their element types
  • Use Callable[[arg_types], return_type] to type functions passed as arguments

Frequently Asked Questions

Why does mypy say Unsupported operand type(s) for +: "int" and "None" when I've already checked for None?

Make sure mypy can see your None check. Simple checks like if x is not None: work; complex logic might confuse mypy. Use explicit checks: if x is None: return or assert x is not None.

Can I ignore a specific mypy error on one line?

Yes, use # type: ignore[error-code]. For example: age = get_age(1) # type: ignore[operator]. To find the error code, add show_error_codes = True to your mypy.ini.

mypy complains about a third-party library type. How do I fix it?

If a library has no type stubs, add it to your mypy.ini: [mypy-library_name] ignore_missing_imports = True. This tells mypy to skip type checking for that library.

What's the difference between error and note in mypy output?

error is a type mismatch that will break type checking; note is additional context. You must fix errors to pass type checking; notes are informational.

Can mypy infer types, or must I annotate everything?

mypy infers local variable types from assignments. You must annotate function parameters and return types (in strict mode), but local variables inside functions are often inferred.

Further Reading