← Back to DevBytes

Fix 'TypeError' in Python: Complete Troubleshooting Guide

Understanding TypeError in Python

A TypeError is one of the most common exceptions you'll encounter as a Python developer. It occurs when an operation or function is applied to an object of an inappropriate type. The Python interpreter raises this exception when it detects that the data type of an operand or argument doesn't match what the operation expects.

The typical error message looks like this:

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Or variations such as:

TypeError: 'int' object is not iterable
TypeError: can only concatenate str (not "int") to str
TypeError: object of type 'NoneType' has no len()
TypeError: 'list' object is not callable

At its core, a TypeError signals a fundamental mismatch between what your code assumes about a value's type and what that type actually is. Understanding how to read, diagnose, and fix these errors is an essential skill for writing robust Python code.

Why TypeError Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

TypeErrors are more than just annoying interruptions. They reveal underlying problems in your code's logic and data handling. Here's why they demand your attention:

1. They Expose Assumption Failures

Every TypeError represents a place where you made an assumption about the shape or type of your data — and that assumption was wrong. For example, you assumed a variable would always be a list, but it turned out to be None. These assumption failures are bugs waiting to happen, and TypeErrors bring them to the surface early.

2. They Help Prevent Silent Data Corruption

In a dynamically typed language like Python, type mismatches can sometimes produce unexpected results rather than immediate errors. Python's strong typing philosophy means many mismatches do raise TypeErrors, which is actually a good thing — it prevents subtle bugs from propagating through your program undetected.

3. They Guide Better Code Design

Every TypeError you encounter and fix teaches you something about designing clearer interfaces. When you find yourself repeatedly checking types or catching TypeErrors, it's a sign that you might benefit from type hints, stricter validation, or a cleaner separation of concerns.

4. They're Common in Production

Unlike syntax errors (which are caught before deployment), TypeErrors frequently occur at runtime in production environments — when unexpected user input, API responses, or edge cases trigger type mismatches. Knowing how to handle them gracefully separates reliable applications from fragile ones.

Common Causes of TypeError (with Examples)

Cause 1: Mixing Incompatible Types in Operations

This is the most frequent trigger. You attempt an arithmetic operation, concatenation, or comparison between types that don't support it.

# Example: Adding int and str
age = 25
message = "I am " + age + " years old"
# TypeError: can only concatenate str (not "int") to str

The fix: Convert one operand to match the other's type explicitly.

# Corrected version
age = 25
message = "I am " + str(age) + " years old"
print(message)  # Output: I am 25 years old

# Alternative using f-strings (cleaner)
message = f"I am {age} years old"

Cause 2: Calling a Non-Callable Object

You use parentheses () on something that isn't a function or method — often because a variable name shadows a function, or you accidentally reassigned a function to a non-callable value.

# Example: Shadowing the built-in list() function
list = [1, 2, 3]
result = list( (4, 5) )
# TypeError: 'list' object is not callable

The fix: Avoid shadowing built-in names. If the damage is done, rename your variable.

# Corrected version
my_list = [1, 2, 3]
result = list( (4, 5) )  # Uses the built-in list() function
print(result)  # Output: [4, 5]

Cause 3: Iterating Over a Non-Iterable Object

You use a for loop, list comprehension, or the in operator on an object that doesn't support iteration — commonly an integer, None, or a custom object without __iter__.

# Example: Trying to loop over an integer
count = 5
for i in count:
    print(i)
# TypeError: 'int' object is not iterable

The fix: Use range() to iterate a specific number of times, or ensure the variable is actually a collection.

# Corrected version using range()
count = 5
for i in range(count):
    print(i)  # Outputs: 0, 1, 2, 3, 4

# Or if count should be a collection
items = [10, 20, 30, 40, 50]
for item in items:
    print(item)

Cause 4: Passing the Wrong Number or Type of Arguments

You call a function with too many positional arguments, too few, or arguments of an unexpected type that the function's internal logic cannot handle.

# Example: Passing a string to a function expecting a number
def square(n):
    return n * n

result = square("5")
# TypeError: can't multiply sequence by non-int of type 'str'

The fix: Validate and convert inputs at the function boundary, or use type hints with a checking tool.

# Corrected version with input validation
def square(n):
    if not isinstance(n, (int, float)):
        raise ValueError(f"Expected a number, got {type(n).__name__}")
    return n * n

result = square(5)       # Works: 25
result = square("5")     # Raises ValueError with clear message

# Better: convert if safe
def square(n):
    try:
        n = float(n)
    except (ValueError, TypeError):
        raise ValueError(f"Cannot convert {n!r} to a number")
    return n * n

Cause 5: Accessing Length or Index on NoneType

A variable you expected to contain a list, string, or dictionary is actually None — often the return value of a function that didn't find a result.

# Example: len() on None
def find_user(username):
    # Simulating a database lookup that fails
    return None

user_data = find_user("alice")
print(len(user_data))
# TypeError: object of type 'NoneType' has no len()

The fix: Always check for None before operating on potentially absent values.

# Corrected version with None check
def find_user(username):
    return None  # Simulating not found

user_data = find_user("alice")
if user_data is not None:
    print(len(user_data))
else:
    print("User not found")

# Or using the walrus operator (Python 3.8+)
if (user_data := find_user("alice")) is not None:
    print(len(user_data))

Cause 6: Using an Index on an Inappropriate Type

You try to subscript (use square brackets) on a type that doesn't support indexing, like an integer or a custom object without __getitem__.

# Example: Subscripting an integer
number = 42
digit = number[0]
# TypeError: 'int' object is not subscriptable

The fix: Convert to a string first if you need individual digits, or reconsider your data structure.

# Corrected version
number = 42
digit = str(number)[0]  # '4'
print(digit)

# For actual digit extraction (mathematical approach)
def get_digit(num, position_from_right):
    return (abs(num) // 10**position_from_right) % 10

print(get_digit(42, 1))  # Output: 4 (tens place)

Cause 7: Unexpected Return Type from Methods

Many methods in Python mutate objects in place and return None. Chaining method calls or assigning the result can lead to TypeErrors when you later treat None as the original type.

# Example: .sort() returns None
numbers = [3, 1, 2]
sorted_numbers = numbers.sort()
print(sorted_numbers[0])
# TypeError: 'NoneType' object is not subscriptable

The fix: Know which methods return a new object and which mutate in place returning None.

# Corrected version
numbers = [3, 1, 2]
numbers.sort()  # Mutates in place, returns None
print(numbers[0])  # Output: 1

# Or use sorted() which returns a new list
numbers = [3, 1, 2]
sorted_numbers = sorted(numbers)  # Returns a new sorted list
print(sorted_numbers[0])  # Output: 1

Systematic Approach to Debugging TypeError

When you encounter a TypeError, follow this structured debugging workflow to isolate and fix the issue quickly:

Step 1: Read the Full Error Message Carefully

Python's TypeError messages are remarkably descriptive. They tell you:

Don't skip reading the message. The types listed are your primary clues.

Step 2: Print Types at Runtime

Before the error line, insert debug prints to inspect the actual types of variables:

# Debugging technique
value = some_function()
print(f"DEBUG: value = {value!r}, type = {type(value).__name__}")
result = value + 10  # If this raises TypeError, you'll know why

Step 3: Trace Back the Variable's Origin

Ask yourself: Where did this variable get its value? Was it:

Step 4: Use isinstance() for Defensive Checks

Add guards before operations that are type-sensitive:

def safe_concatenate(a, b):
    if not isinstance(a, str) or not isinstance(b, str):
        a = str(a)
        b = str(b)
    return a + b

print(safe_concatenate("Hello ", 42))  # Output: Hello 42

Step 5: Employ try/except for Graceful Degradation

When you cannot guarantee types (e.g., parsing user input), wrap the operation in a try/except block:

def parse_and_sum(items):
    total = 0
    for item in items:
        try:
            total += float(item)
        except (TypeError, ValueError):
            print(f"Skipping non-numeric item: {item!r}")
    return total

data = [10, "20", "abc", 30.5, None]
result = parse_and_sum(data)
print(f"Total: {result}")  # Output: Total: 60.5

Preventive Best Practices

1. Adopt Type Hints

Type hints don't enforce types at runtime, but they document expectations clearly and work with static type checkers like mypy to catch mismatches before execution.

from typing import List, Optional

def get_user_ids(users: List[str]) -> List[int]:
    """Convert user string IDs to integers."""
    return [int(u) for u in users]

def find_item(key: str) -> Optional[str]:
    """Returns None if item not found."""
    cache = {"name": "Alice"}
    return cache.get(key)

Run mypy on your codebase regularly to catch type mismatches:

# Terminal command
$ mypy my_script.py
# my_script.py:5: error: Argument 1 to "get_user_ids" has incompatible type "int"; expected "List[str]"

2. Validate Data at Boundaries

The edges of your system — user input, file reads, API responses, database queries — are where type uncertainty is highest. Validate and coerce types at these boundaries so the rest of your code can operate with confidence.

def process_order(raw_data: dict) -> dict:
    """Validate and clean order data from external source."""
    # Coerce quantity to int, defaulting to 0 on failure
    try:
        quantity = int(raw_data.get("quantity", 0))
    except (TypeError, ValueError):
        quantity = 0
    
    # Coerce price to float
    try:
        price = float(raw_data.get("price", 0.0))
    except (TypeError, ValueError):
        price = 0.0
    
    return {"quantity": quantity, "price": price, "total": quantity * price}

3. Use isinstance() Over type() Comparisons

isinstance() respects inheritance hierarchies and is the Pythonic way to check types:

# Good: isinstance() works with subclasses
if isinstance(value, (int, float)):
    result = value * 2

# Less flexible: type() exact match
if type(value) == int:  # Misses float, complex, numpy int types
    result = value * 2

# Better: duck typing when possible
try:
    result = value * 2
except TypeError:
    result = None

4. Leverage Default Values and the "or" Pattern

For optional values that might be None, use defaults:

# Using 'or' for fallback (careful: treats empty sequences as falsy too)
items = maybe_list or []
for item in items:
    print(item)

# More precise None check
items = maybe_list if maybe_list is not None else []

5. Avoid Shadowing Built-ins

Never name variables list, str, dict, set, type, id, max, min, sum, or any other built-in function or type. This prevents the confusing "X object is not callable" TypeError.

# Dangerous shadowing
list = [1, 2, 3]
str = "hello"
type = int

# Safe naming
my_list = [1, 2, 3]
text = "hello"
number_type = int

6. Understand Mutating Methods vs. Returning Methods

Create a mental model of which methods return a new object and which mutate in place:

7. Use Assertions During Development

Assertions help catch type mismatches early in development and testing:

def calculate_average(numbers):
    assert isinstance(numbers, list), "Expected a list"
    assert all(isinstance(n, (int, float)) for n in numbers), "All elements must be numbers"
    return sum(numbers) / len(numbers) if numbers else 0

Note: Assertions can be disabled with the -O flag in production, so use them alongside proper validation for critical paths.

Real-World Scenarios and Solutions

Scenario 1: JSON API Response with Missing Fields

import json
from typing import Optional, Dict, Any

def extract_username(api_response: str) -> Optional[str]:
    """Safely extract username from JSON API response."""
    try:
        data: Dict[str, Any] = json.loads(api_response)
    except json.JSONDecodeError:
        print("Invalid JSON received")
        return None
    
    # Safely navigate nested structure with .get() (returns None if missing)
    user = data.get("user")
    if user is None:
        return None
    
    # user might be a dict, but could be a list or string in malformed data
    if not isinstance(user, dict):
        print(f"Expected dict for 'user', got {type(user).__name__}")
        return None
    
    return user.get("username")

Scenario 2: DataFrame or Array Operations

# Working with pandas or numpy — type mismatches are common
import pandas as pd
import numpy as np

df = pd.DataFrame({
    'values': [10, '20', 30, 'invalid', 40]
})

# Safe conversion with error handling
def safe_to_numeric(series):
    """Convert series to numeric, coercing errors to NaN."""
    return pd.to_numeric(series, errors='coerce')

df['clean_values'] = safe_to_numeric(df['values'])
print(df)
#       values  clean_values
# 0         10          10.0
# 1         20          20.0
# 2         30          30.0
# 3    invalid           NaN
# 4         40          40.0

Scenario 3: Function Composition with Type Transformations

from typing import List, Union

def flatten_and_sum(nested: List[Union[int, float, List]]) -> float:
    """Recursively flatten nested lists and sum all numbers."""
    total = 0.0
    
    for item in nested:
        if isinstance(item, list):
            # Recurse into nested lists
            total += flatten_and_sum(item)
        elif isinstance(item, (int, float)):
            total += float(item)
        else:
            # Skip non-numeric items gracefully
            print(f"Skipping {item!r} of type {type(item).__name__}")
    
    return total

data = [1, [2, 3], "not_a_number", [4, [5, "six"]]]
result = flatten_and_sum(data)
print(f"Sum: {result}")  # Output: Sum: 15.0

Advanced Type Checking with mypy and Type Guards

For larger codebases, integrate static type checking into your development workflow:

# example.py
from typing import Union, List

def process_value(value: Union[int, str, List[int]]) -> str:
    """Process a value that could be multiple types."""
    if isinstance(value, int):
        return f"Integer: {value * 2}"
    elif isinstance(value, str):
        return f"String: {value.upper()}"
    elif isinstance(value, list):
        return f"List sum: {sum(value)}"
    else:
        # This branch is unreachable with correct typing, but handles edge cases
        raise TypeError(f"Unexpected type: {type(value).__name__}")

# mypy validates that all branches are covered

Run static analysis regularly:

$ mypy --strict example.py
Success: no issues found in 1 source file

Conclusion

TypeError in Python is not your enemy — it's a precise diagnostic tool that reveals where your assumptions about data types diverge from reality. By reading error messages carefully, tracing variable origins, and applying the systematic debugging workflow outlined above, you can resolve TypeErrors efficiently and prevent them from recurring. The best defense combines multiple strategies: type hints for documentation and static checking, validation at system boundaries, defensive isinstance() checks where needed, and a solid understanding of which Python methods mutate in place versus returning new objects. As you integrate these practices into your daily development, you'll find that TypeErrors shift from frustrating interruptions to valuable early warnings that help you build more resilient, maintainable Python applications.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles