ZetCode

Python type Function

Last modified March 26, 2025

This comprehensive guide delves into Python's type function, a versatile tool that serves two primary roles: identifying an object's type and enabling dynamic class creation. Through practical examples, we'll explore its applications in type checking, metaprogramming, and runtime class manipulation, highlighting its significance in Python's dynamic typing system.

Basic Type Checking

The most straightforward application of type is to ascertain an object's type, providing insight into its underlying class.

basic_type.py
num = 42
name = "Alice"
lst = [1, 2, 3]

print(type(num))   # <class 'int'>
print(type(name))  # <class 'str'>
print(type(lst))   # <class 'list'>

In this example, type reveals the class of each variable: num is an integer (int), name is a string (str), and lst is a list (list). The function returns a type object, which Python displays as, for instance, <class 'int'>. This is invaluable for debugging, ensuring variables conform to expected types, or exploring unfamiliar objects.

This basic usage highlights that integer literals instantiate int, strings instantiate str, and lists instantiate list. Such type introspection aids in understanding Python's dynamic typing and is particularly useful when inspecting data during development or troubleshooting.

Type Comparison

The type function can be used to directly compare an object's type against a specific class, enabling precise type verification.

type_comparison.py
value = 3.14

if type(value) == float:
    print("It's a float")
elif type(value) == int:
    print("It's an integer")
else:
    print("Unknown type")

Here, type(value) is compared with float and int using equality (==). For value = 3.14, the output is "It's a float" because type(3.14) matches float. This approach ensures an exact match, ignoring inheritance, unlike isinstance, which is more permissive.

While effective, this method is less flexible than isinstance, which accounts for subclass relationships. Use type for comparisons when an exact type match is critical, such as distinguishing between float and int without considering derived types. It's a precise tool for strict type enforcement in specific scenarios.

Dynamic Class Creation

Beyond type checking, type's three-argument form allows dynamic creation of classes at runtime, offering powerful metaprogramming capabilities.

dynamic_class.py
def greet(self):
    return f"Hello, {self.name}"

Person = type('Person', (), {
    '__init__': lambda self, name: setattr(self, 'name', name),
    'greet': greet
})

p = Person("Alice")
print(p.greet())  # Hello, Alice

This code dynamically constructs a Person class using type. The arguments are: the class name ("Person"), an empty tuple of base classes (no inheritance), and a dictionary defining methods. The __init__ lambda sets the name attribute, and greet uses it. Instantiating Person("Alice") and calling p.greet outputs "Hello, Alice".

The three arguments to type are the class name as a string, a tuple of base classes, and a dictionary of attributes and methods. This mirrors how Python internally processes class definitions, making type a foundational tool for runtime class generation. It's ideal for frameworks or scenarios requiring programmatic class construction.

Metaclass Basics

In Python, type is the default metaclass—the class of all classes— underpinning the language's object model.

metaclass_basic.py
class Animal:
    pass

print(type(Animal))  # <class 'type'>
print(type(type))    # <class 'type'>

This example reveals that Animal, a user-defined class, has type as its type, outputting <class 'type'>. Similarly, type(type) shows that type is its own metaclass, a recursive relationship unique to Python. This demonstrates that all classes are instances of type.

Understanding type as a metaclass clarifies Python's type system. Classes are objects too, and type governs their creation and behavior. This self-referential nature (type being an instance of itself) is a cornerstone of Python's metaprogramming capabilities, enabling advanced customization.

Custom Metaclass

By subclassing type, you can craft custom metaclasses to tailor class creation, adding attributes or modifying behavior.

custom_metaclass.py
class Meta(type):

    def __new__(cls, name, bases, namespace):
        namespace['version'] = 1.0
        return super().__new__(cls, name, bases, namespace)

class MyClass(metaclass=Meta):
    pass

print(MyClass.version)  # 1.0

The Meta metaclass overrides __new__ to inject a version attribute into the namespace of any class it creates. When MyClass is defined with metaclass=Meta, it inherits this attribute, accessible as MyClass.version. The super call ensures standard class creation proceeds after customization.

This metaclass automatically endows classes with a version attribute, demonstrating how type subclassing can enforce conventions or add metadata. The __new__ method, invoked during class construction, provides a hook for such modifications, offering fine- grained control over class definitions.

Type Checking for Built-ins

The type function consistently identifies the types of Python's built-in objects, showcasing the language's type diversity.

builtin_types.py
types = [
    42, 3.14, True, "hello",
    [1, 2], (1, 2), {1, 2},
    {'a': 1}, range(5), type
]

for obj in types:
    print(f"{str(obj):<10} is {type(obj)}")

This script iterates over a list of built-in objects, printing each object's value and type. Outputs include <class 'int'> for 42, <class 'float'> for 3.14, and so forth, up to <class 'type'> for type itself. The :<10 format aligns the output for readability.

This example illustrates type's reliability across Python's core types, from numbers and sequences to mappings and the type metaclass. It's a practical way to explore Python's type system, useful for educational purposes or verifying type assumptions in complex codebases.

Dynamic Class Modification

Using type, you can dynamically alter existing classes or create modified versions at runtime, enhancing flexibility.

dynamic_modification.py
class Base:
    pass

def new_method(self):
    return "Dynamically added"

Modified = type('Modified', (Base,), {'new_method': new_method})

m = Modified()
print(m.new_method())  # Dynamically added

This code defines a simple Base class and a function new_method. Using type, it creates a new class, Modified, inheriting from Base and adding new_method. An instance of Modified can then call this method, producing "Dynamically added".

This technique excels at adding methods at runtime, crafting specialized class variants, or implementing plugin systems. It leverages type's ability to construct classes dynamically, providing a powerful mechanism for extending functionality without altering original class definitions.

Type Introspection with Custom Classes

The type function is equally effective with user-defined classes, enabling detailed introspection of custom objects.

custom_type.py
class Vehicle:
    def __init__(self, make):
        self.make = make

car = Vehicle("Toyota")
print(type(car))         # <class '__main__.Vehicle'>
print(type(Vehicle))     # <class 'type'>

In this example, Vehicle is a custom class with a make attribute. For an instance car, type(car) returns &lt;class '__main__.Vehicle'&gt;, indicating its class. For the class itself, type(Vehicle) yields &lt;class 'type'&gt;, affirming type as its metaclass.

This demonstrates type's utility in examining both instances and classes in user-defined contexts. It distinguishes between an object's type and the type of its class, reinforcing the metaclass concept and aiding in debugging or runtime type analysis of custom structures.

Using type in Function Arguments

The type function can validate or process arguments based on their types within functions, enhancing robustness.

type_in_function.py
def process_data(data):
    if type(data) == list:
        return sum(data)
    elif type(data) == str:
        return data.upper()
    else:
        return f"Unsupported type: {type(data)}"

print(process_data([1, 2, 3]))  # 6
print(process_data("hello"))    # HELLO
print(process_data(42))         # Unsupported type: <class 'int'>

This function, process_data, uses type to check the argument's type. For a list, it computes the sum; for a string, it converts to uppercase; otherwise, it reports the unsupported type. The outputs reflect these behaviors: 6 for a list, "HELLO" for a string, and a message for an integer.

Incorporating type in functions allows type-specific logic, useful for handling diverse inputs safely. While isinstance is often preferred for broader type checking, type ensures exact matches, making it suitable for strict type-based operations or error reporting.

Type-Based Dispatching

The type function can drive type-based dispatching, selecting behavior based on an object's exact type.

type_dispatch.py
handlers = {
    int: lambda x: x * 2,
    str: lambda x: x + "!",
    list: lambda x: x + [0]
}

def dispatch(value):
    handler = handlers.get(type(value))
    return handler(value) if handler else "No handler"

print(dispatch(5))        # 10
print(dispatch("hi"))     # hi!
print(dispatch([1, 2]))   # [1, 2, 0]

This code defines a dictionary, handlers, mapping types to lambda functions. The dispatch function uses type(value) to fetch the appropriate handler and applies it. Outputs are 10 (integer doubled), "hi!" (string appended), and [1, 2, 0] (list extended), with a fallback for unhandled types.

Type-based dispatching with type offers a clean way to implement polymorphic behavior without subclassing. It's precise, relying on exact type matches, and suits scenarios like data processing pipelines or extensible systems where specific type handling is required.

Best Practices

Source References

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.