Python - Check Type of Variable (type, isinstance)

Python's dynamic typing gives you flexibility, but that flexibility comes with responsibility. Variables can hold any type, and nothing stops you from passing a string where a function expects a...

Key Insights

  • Use isinstance() for type checking in production code—it respects inheritance and handles multiple types cleanly, while type() is better for debugging and exact type inspection.
  • Check against abstract base classes from collections.abc when you care about behavior rather than concrete types—this embraces Python’s duck typing philosophy.
  • Type checking should be a safety net, not a crutch—excessive runtime type checks often indicate design problems that type hints and better architecture could solve.

Why Type Checking Matters in Python

Python’s dynamic typing gives you flexibility, but that flexibility comes with responsibility. Variables can hold any type, and nothing stops you from passing a string where a function expects a list. When things go wrong, you need to inspect types for debugging. When building robust APIs, you need to validate inputs. When processing heterogeneous data, you need conditional logic based on types.

Python provides two primary tools for runtime type checking: type() and isinstance(). Understanding when to use each—and when to avoid type checking altogether—separates competent Python developers from those who fight against the language.

Using the type() Function

The type() function returns the exact class of an object. No inheritance, no duck typing—just the precise type.

# Basic type inspection
x = 42
y = "hello"
z = [1, 2, 3]
d = {"key": "value"}

print(type(x))  # <class 'int'>
print(type(y))  # <class 'str'>
print(type(z))  # <class 'list'>
print(type(d))  # <class 'dict'>

You can compare types directly using ==:

x = 42

if type(x) == int:
    print("x is an integer")

# Comparing against type objects
print(type(x) == int)    # True
print(type(x) == str)    # False
print(type(x) == float)  # False

For checking multiple types with type(), you need explicit comparisons:

def describe_type(value):
    t = type(value)
    if t == int:
        return "integer"
    elif t == float:
        return "floating point"
    elif t == str:
        return "string"
    elif t == list:
        return "list"
    else:
        return "unknown"

print(describe_type(42))        # integer
print(describe_type(3.14))      # floating point
print(describe_type("hello"))   # string

The type() function excels at debugging and introspection. When you’re in a REPL trying to understand what you’re working with, type() gives you the unambiguous answer.

Using the isinstance() Function

The isinstance() function checks whether an object is an instance of a class or any of its subclasses. It returns a boolean and accepts either a single type or a tuple of types.

x = 42
y = 3.14
z = "hello"

# Single type check
print(isinstance(x, int))    # True
print(isinstance(y, float))  # True
print(isinstance(z, str))    # True

# Check against multiple types
print(isinstance(x, (int, float)))  # True
print(isinstance(y, (int, float)))  # True
print(isinstance(z, (int, float)))  # False

The tuple syntax makes isinstance() cleaner for validating against multiple acceptable types:

def process_number(value):
    if not isinstance(value, (int, float)):
        raise TypeError(f"Expected number, got {type(value).__name__}")
    return value * 2

print(process_number(5))      # 10
print(process_number(2.5))    # 5.0
# process_number("5")         # Raises TypeError

Key Differences: type() vs isinstance()

The critical difference emerges with inheritance. type() checks for exact type matches, while isinstance() respects the inheritance hierarchy.

class CustomList(list):
    def sum(self):
        return sum(self)

my_list = CustomList([1, 2, 3])

# type() checks exact type only
print(type(my_list) == list)         # False
print(type(my_list) == CustomList)   # True

# isinstance() respects inheritance
print(isinstance(my_list, list))         # True
print(isinstance(my_list, CustomList))   # True

This matters in real code. Consider a function that processes lists:

class SortedList(list):
    def append(self, item):
        super().append(item)
        self.sort()

def process_items(items):
    # Bad: breaks with subclasses
    if type(items) == list:
        return [x * 2 for x in items]
    raise TypeError("Expected a list")

def process_items_better(items):
    # Good: works with any list subclass
    if isinstance(items, list):
        return [x * 2 for x in items]
    raise TypeError("Expected a list")

regular = [1, 2, 3]
sorted_list = SortedList([3, 1, 2])

print(process_items(regular))           # [2, 4, 6]
# print(process_items(sorted_list))     # Raises TypeError!

print(process_items_better(regular))    # [2, 4, 6]
print(process_items_better(sorted_list))  # [2, 4, 6] - works!

The rule is simple: Use isinstance() for type checking in application logic. Use type() when you specifically need the exact type, typically for debugging or serialization.

Practical Use Cases

Input Validation

Validate function arguments early to provide clear error messages:

def create_user(name, age, tags=None):
    if not isinstance(name, str):
        raise TypeError(f"name must be str, got {type(name).__name__}")
    if not isinstance(age, int) or isinstance(age, bool):  # bool is subclass of int!
        raise TypeError(f"age must be int, got {type(age).__name__}")
    if tags is not None and not isinstance(tags, list):
        raise TypeError(f"tags must be list or None, got {type(tags).__name__}")
    
    return {
        "name": name,
        "age": age,
        "tags": tags or []
    }

# Note the bool check - True/False are technically ints in Python
print(isinstance(True, int))  # True - this can bite you!

Processing Mixed-Type Collections

Handle heterogeneous data structures gracefully:

def flatten_values(data):
    """Extract all scalar values from a nested structure."""
    results = []
    
    if isinstance(data, dict):
        for value in data.values():
            results.extend(flatten_values(value))
    elif isinstance(data, (list, tuple)):
        for item in data:
            results.extend(flatten_values(item))
    elif isinstance(data, (str, int, float, bool)):
        results.append(data)
    # Skip None and other types
    
    return results

nested = {
    "users": [
        {"name": "Alice", "scores": [95, 87, 92]},
        {"name": "Bob", "scores": [88, 91, 85]}
    ],
    "metadata": {"version": 1, "active": True}
}

print(flatten_values(nested))
# ['Alice', 95, 87, 92, 'Bob', 88, 91, 85, 1, True]

Type-Based Dispatch

Route processing based on input types:

def serialize(value):
    """Convert Python objects to JSON-compatible format."""
    if value is None:
        return None
    elif isinstance(value, bool):  # Check bool before int!
        return value
    elif isinstance(value, (int, float)):
        return value
    elif isinstance(value, str):
        return value
    elif isinstance(value, (list, tuple)):
        return [serialize(item) for item in value]
    elif isinstance(value, dict):
        return {str(k): serialize(v) for k, v in value.items()}
    elif hasattr(value, '__dict__'):
        return serialize(vars(value))
    else:
        return str(value)

Type Checking with Abstract Base Classes

Python’s collections.abc module provides abstract base classes that let you check for behavior rather than concrete types. This aligns with duck typing—if it walks like a duck and quacks like a duck, treat it as a duck.

from collections.abc import Iterable, Mapping, Callable, Sequence

# Check if something is iterable (has __iter__)
print(isinstance([1, 2, 3], Iterable))      # True
print(isinstance("hello", Iterable))         # True
print(isinstance({"a": 1}, Iterable))        # True
print(isinstance(range(10), Iterable))       # True
print(isinstance(42, Iterable))              # False

# Check if something is a mapping (dict-like)
print(isinstance({"a": 1}, Mapping))         # True
print(isinstance([1, 2], Mapping))           # False

# Check if something is callable
print(isinstance(len, Callable))             # True
print(isinstance(lambda x: x, Callable))     # True
print(isinstance("string", Callable))        # False

This approach makes your code more flexible:

from collections.abc import Iterable, Mapping

def process_config(config):
    """Accept any mapping type, not just dict."""
    if not isinstance(config, Mapping):
        raise TypeError("config must be a mapping type")
    
    return {k.upper(): v for k, v in config.items()}

def safe_iterate(data, default=None):
    """Safely iterate over anything iterable."""
    if not isinstance(data, Iterable):
        return default if default is not None else []
    
    # Don't iterate over strings character by character
    if isinstance(data, str):
        return [data]
    
    return list(data)

# Works with dict, OrderedDict, ChainMap, custom mappings...
from collections import OrderedDict
print(process_config(OrderedDict([("host", "localhost"), ("port", 8080)])))
# {'HOST': 'localhost', 'PORT': 8080}

Conclusion

Choose your type checking tool based on the situation:

  • Use isinstance() for runtime type validation in production code. It respects inheritance, handles multiple types elegantly, and works with abstract base classes for duck typing.

  • Use type() for debugging, logging, and cases where you need the exact type. It’s also useful when you explicitly want to exclude subclasses.

  • Use abstract base classes from collections.abc when you care about capabilities rather than concrete types. Checking for Iterable instead of list makes your code more flexible and Pythonic.

That said, consider whether you need runtime type checking at all. Modern Python offers type hints that enable static analysis without runtime overhead:

from typing import Union

def add_numbers(a: int | float, b: int | float) -> float:
    return float(a + b)

Tools like mypy catch type errors before your code runs. Runtime type checking still has its place—especially for validating external input, building frameworks, or debugging—but it shouldn’t be your first line of defense. Write clear interfaces, use type hints, and reserve isinstance() for the boundaries where untrusted data enters your system.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.