Skip to main content

Python Everything Is an Object Practice Problems & Exercises

Practice: Everything Is an Object

12 problems4 Easy5 Medium3 Hard45–60 min
← Back to lesson

Easy

#1Functions Are ObjectsEasy
first-class functionsattributesassignment

Prove that functions in Python are full objects: they have a type, can be assigned to variables, and carry attributes.

Python
def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}!"

print(type(greet))

say_hi = greet
print(say_hi("World"))

print(greet.__doc__)
Solution
def greet(name):
"""Say hello to someone."""
return f"Hello, {name}!"

# 1. Functions have a type
print(type(greet)) # <class 'function'>

# 2. Functions can be assigned to variables
say_hi = greet
print(say_hi("World")) # Hello, World!

# 3. Functions carry attributes like any object
print(greet.__doc__) # Say hello to someone.

Why this matters: In Python, functions are first-class objects. They are instances of the function class, can be passed around, stored in data structures, and carry attributes like __doc__, __name__, __module__, and __defaults__. This is the foundation for decorators, callbacks, and higher-order programming in Python.

def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}!"

# TODO: Prove that functions are objects
# 1. Print the type of greet
# 2. Assign greet to another variable and call it
# 3. Print an attribute that only objects have (e.g., __doc__)
Expected Output
<class 'function'>
Hello, World!
Say hello to someone.
Hints

Hint 1: Functions have a type — try type(greet).

Hint 2: You can assign a function to a new name like any other object: say_hi = greet.

Hint 3: Every function object has a __doc__ attribute holding its docstring.

#2The Type of TypeEasy
typemetaclassobject-model

Predict the output, then run to verify. What is the type of type itself?

Python
print(type(type))
print(type(type(type)))
print(type is type(type))
print(isinstance(type, type))
Solution
<class 'type'>
<class 'type'>
True
True

Explanation:

  • type(type) returns <class 'type'>type is an instance of itself. This is the bootstrap that makes Python's object model work.
  • type(type(type)) is still <class 'type'> — the chain never ends, it is type all the way down.
  • type is type(type) is Truetype and its own type are the same object.
  • isinstance(type, type) is Truetype is an instance of type.

Key insight: Python has a circular bootstrap: type is an instance of type, and type is a subclass of object, but object is an instance of type. This circularity is created at the C level when the interpreter starts up.

Expected Output
<class 'type'>
<class 'type'>
True
True
Hints

Hint 1: type() returns the type of any object. What happens when you call type(type)?

Hint 2: type is its own metaclass — it is an instance of itself.

#3Modules Are Objects TooEasy
modulestypeattributes

Demonstrate that imported modules are regular Python objects with a type and attributes.

Python
import math

print(type(math))
print(math.__name__)
print(hasattr(math, '__doc__'))
Solution
import math

# 1. Modules have a type
print(type(math)) # <class 'module'>

# 2. Modules have a __name__ attribute
print(math.__name__) # math

# 3. Modules have all the standard object attributes
print(hasattr(math, '__doc__')) # True

Why this matters: When you write import math, Python creates a module object and binds it to the name math in your namespace. Modules are objects — you can pass them to functions, store them in lists, inspect them with dir(), and even modify their attributes at runtime. This is why patterns like importlib.import_module() work: they return module objects just like any other value.

import math
import os

# TODO: Prove that modules are objects
# 1. Print the type of the math module
# 2. Show that modules have attributes using dir()
# 3. Access the module's __name__ and __file__ attributes
Expected Output
<class 'module'>
math
True
Hints

Hint 1: Try type(math) to see what kind of object a module is.

Hint 2: Modules have __name__, __file__, __doc__, and __spec__ attributes like any object.

#4Everything Inherits from ObjectEasy
isinstanceobjectinheritance

Verify that every value in Python is an instance of object — integers, strings, None, functions, classes, and even object itself.

Python
things = [
    42, 3.14, "hello", True, None,
    [1, 2], (3, 4), {5: 6}, {7, 8},
    lambda x: x, int, type, object
]

for thing in things:
    name = repr(thing) if not callable(thing) or isinstance(thing, type) else type(thing).__name__
    label = repr(thing)[:20]
    print(f"{label} ({type(thing).__name__}): {isinstance(thing, object)}")
Solution
things = [
42, 3.14, "hello", True, None,
[1, 2], (3, 4), {5: 6}, {7, 8},
lambda x: x, int, type, object
]

for thing in things:
label = repr(thing)[:20]
print(f"{label} ({type(thing).__name__}): {isinstance(thing, object)}")

Every single line prints True.

Key insight: In Python 3, object is the ultimate base class of everything. Every value — whether it is a primitive like 42, a container like [1, 2], a function, a class, or even object itself — is an instance of object. This is what "everything is an object" literally means: every value in Python's runtime has object at the root of its inheritance chain.

The proof: isinstance(object, object) returns True. Even object is an instance of itself — this is part of the same bootstrap circularity as type(type) is type.

# TODO: Verify that ALL of these are instances of object
things = [42, 3.14, "hello", True, None, [1,2], (3,4), {5:6}, {7,8}, lambda x: x, int, type, object]

# Check each one and print the result
Expected Output
42 (int): True\n3.14 (float): True\n'hello' (str): True\nTrue (bool): True\nNone (NoneType): True\n[1, 2] (list): True\n(3, 4) (tuple): True\n{5: 6} (dict): True\n{8, 7} (set): True\n<lambda> (function): True\nint (type): True\ntype (type): True\nobject (type): True
Hints

Hint 1: isinstance(x, object) checks if x is an instance of the object class.

Hint 2: In Python 3, every single value — including None, classes, and type itself — is an instance of object.


Medium

#5Function Dispatch TableMedium
first-class functionsdictdispatch

Build a calculator that uses a dispatch table — a dictionary mapping operation names to functions. This is a core pattern that exploits the fact that functions are objects.

# After implementing calculate():
print("add:", calculate("add", 10, 5))
print("subtract:", calculate("subtract", 10, 5))
print("multiply:", calculate("multiply", 10, 5))
print("divide:", calculate("divide", 10, 5))
print("unknown:", calculate("power", 10, 5))
Solution
def add(a, b):
return a + b

def subtract(a, b):
return a - b

def multiply(a, b):
return a * b

def divide(a, b):
if b == 0:
return "Error: division by zero"
return a / b

# Dispatch table: maps string names to function objects
dispatch = {
"add": add,
"subtract": subtract,
"multiply": multiply,
"divide": divide,
}

def calculate(operation, a, b):
func = dispatch.get(operation)
if func is None:
return f"Error: unknown operation '{operation}'"
return func(a, b)

print("add:", calculate("add", 10, 5))
print("subtract:", calculate("subtract", 10, 5))
print("multiply:", calculate("multiply", 10, 5))
print("divide:", calculate("divide", 10, 5))
print("unknown:", calculate("power", 10, 5))

Why this is powerful: The dispatch table replaces a chain of if/elif statements with a dictionary lookup. This works because functions are objects — you can store them as values, retrieve them, and call them dynamically. Dispatch tables are used everywhere in production code: web framework routing, CLI argument handling, event systems, and protocol parsers.

Performance note: Dictionary lookup is O(1) average case. An if/elif chain is O(n). For large numbers of operations, dispatch tables are both cleaner and faster.

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

def divide(a, b):
    if b == 0:
        return "Error: division by zero"
    return a / b

# TODO: Create a dispatch table (dict mapping operation names to functions)
# Then use it to execute operations by name

def calculate(operation, a, b):
    """Look up the operation in a dispatch table and call it."""
    pass
Expected Output
add: 15\nsubtract: 5\nmultiply: 50\ndivide: 2.0\nunknown: Error: unknown operation 'power'
Hints

Hint 1: Store functions in a dict: {"add": add, "subtract": subtract, ...}

Hint 2: Look up the function with dispatch.get(operation) and call the result.

Hint 3: Handle unknown operations gracefully with dict.get() and a default.

#6Custom Function AttributesMedium
function attributesfirst-class objectsstate

Python functions are objects, so you can attach custom attributes to them. Create a function that tracks its own call count by storing a counter attribute directly on the function object.

# After implementing:
print(counted_greeter("Alice"))
print(counted_greeter("Bob"))
print(counted_greeter("Charlie"))
print(f"Total calls: {counted_greeter.call_count}")
Solution
def counted_greeter(name):
"""Greet someone and track call count."""
counted_greeter.call_count += 1
return f"Hello, {name}! (call #{counted_greeter.call_count})"

# Initialize the attribute on the function object
counted_greeter.call_count = 0

print(counted_greeter("Alice"))
print(counted_greeter("Bob"))
print(counted_greeter("Charlie"))
print(f"Total calls: {counted_greeter.call_count}")

Why this works: Functions are objects, and like any object in Python, you can set arbitrary attributes on them. counted_greeter.call_count is just a regular attribute on the function object — no different from setting my_obj.x = 5 on a class instance.

When to use this pattern: Function attributes are useful for lightweight state that belongs to the function itself — counters, caches, configuration flags. The functools.lru_cache decorator uses a similar approach internally, attaching cache_info() and cache_clear() methods to the wrapped function.

Caveat: For more complex state, use a class with __call__ instead. Function attributes work but can be surprising to readers who are not expecting them.

# TODO: Create a function that tracks how many times it has been called
# by storing a counter as a custom attribute on the function object itself.

def counted_greeter(name):
    """Greet someone and track call count."""
    pass
Expected Output
Hello, Alice! (call #1)\nHello, Bob! (call #2)\nHello, Charlie! (call #3)\nTotal calls: 3
Hints

Hint 1: You can set arbitrary attributes on function objects: my_func.counter = 0

Hint 2: Initialize the attribute after the function definition, then increment it inside the function body.

Hint 3: Access the function by name inside its own body to read/write its attributes.

#7Exploring Objects with dir()Medium
dirintrospectiondunder methods

Write a function that uses dir() to introspect any object and categorize its attributes into three groups: public, dunder (magic methods), and private.

# After implementing:
for obj in [42, [1, 2, 3]]:
public, dunder, private = categorize_attributes(obj)
print(f"=== {type(obj).__name__} ({obj!r}) ===")
print(f"Public: {public}")
print(f"Dunder: {len(dunder)} attributes")
print(f"Private: {len(private)} attributes")
print()
Solution
def categorize_attributes(obj):
"""Categorize an object's attributes into public, dunder, and private."""
public = []
dunder = []
private = []

for name in dir(obj):
if name.startswith("__") and name.endswith("__"):
dunder.append(name)
elif name.startswith("_"):
private.append(name)
else:
public.append(name)

return sorted(public), sorted(dunder), sorted(private)

for obj in [42, [1, 2, 3]]:
public, dunder, private = categorize_attributes(obj)
print(f"=== {type(obj).__name__} ({obj!r}) ===")
print(f"Public: {public}")
print(f"Dunder: {len(dunder)} attributes")
print(f"Private: {len(private)} attributes")
print()

What this reveals:

  • Even a simple integer like 42 has 68+ dunder methods__add__, __mul__, __eq__, __hash__, etc. These are the methods that make operators work: 42 + 8 is actually (42).__add__(8).
  • A list has 11 public methods (append, sort, etc.) and 39+ dunder methods.
  • Built-in types have zero private attributes — the underscore convention is mainly used in user-defined classes.

Key insight: dir() is the introspection gateway. Combined with getattr(), hasattr(), and callable(), it lets you write code that dynamically explores and interacts with any object — the foundation of plugin systems, serializers, and debugging tools.

# TODO: Write a function that takes any object and returns:
# 1. A list of its "public" attributes (not starting with _)
# 2. A list of its "dunder" attributes (__x__)
# 3. A list of its "private" attributes (_x but not __x__)

def categorize_attributes(obj):
    """Categorize an object's attributes into public, dunder, and private."""
    pass
Expected Output
=== int (42) ===\nPublic: ['bit_count', 'bit_length', 'conjugate', 'denominator', 'imag', 'numerator', 'real']\nDunder: 68 attributes\nPrivate: 0 attributes\n\n=== list ([1, 2, 3]) ===\nPublic: ['append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']\nDunder: 39 attributes\nPrivate: 0 attributes
Hints

Hint 1: Use dir(obj) to get all attribute names as strings.

Hint 2: Dunder attributes start and end with double underscores: name.startswith("__") and name.endswith("__")

Hint 3: Private attributes start with _ but are not dunders.

#8Classes Are ObjectsMedium
metaclasstypefirst-class classes

Predict the output. Classes themselves are objects — what type are they?

Python
class Dog:
    def bark(self):
        return "Woof!"

class Cat:
    def meow(self):
        return "Meow!"

# What is the type of a class?
print(type(Dog))
print(type(Cat))
print(type(int))

# Are classes instances of type?
print(isinstance(Dog, type))
print(isinstance(int, type))
print(isinstance(str, type))

# Classes have attributes like any object
print(Dog.__name__)

# You can store classes in variables and use them
animal_class = Dog
pet = animal_class()
print(type(pet))
Solution
<class 'type'>
<class 'type'>
<class 'type'>
True
True
True
Dog
<class '__main__.Dog'>

Explanation:

  • Every class — whether user-defined (Dog, Cat) or built-in (int, str) — is an instance of type. The type object is the metaclass: the class that creates classes.
  • isinstance(Dog, type) is True because Dog is an object created by type.
  • Classes have attributes: __name__ (the class name as a string), __bases__ (parent classes), __dict__ (namespace), __mro__ (method resolution order).
  • You can assign a class to a variable (animal_class = Dog) and use it to create instances. This is the basis of the factory pattern in Python.

Key insight: The statement class Dog: ... is syntactic sugar for Dog = type('Dog', (object,), {'bark': bark_func}). Classes are literally created by calling type() as a constructor. This is why Python supports metaclasses — you can customize class creation by subclassing type.

Expected Output
<class 'type'>
<class 'type'>
<class 'type'>
True
True
True
Dog
<class '__main__.Dog'>
Hints

Hint 1: type(SomeClass) tells you what kind of object a class is.

Hint 2: All classes are instances of type — that is what makes them classes.

Hint 3: Classes have attributes like __name__, __bases__, and __dict__ just like any object.

#9Function RegistryMedium
first-class functionsdecorator patternregistry

Build a function registry — a system where functions register themselves with a name and description, and can be looked up and called dynamically. This pattern is used in web frameworks, CLI tools, and plugin systems.

# After implementing:
# Should be able to register, list, and call functions by name
Solution
registry = {}

def register(name=None, description="No description"):
"""Register a function in the global registry with optional metadata."""
def decorator(func):
reg_name = name if name is not None else func.__name__
registry[reg_name] = {
"func": func,
"description": description,
}
return func
return decorator

@register(name="greet", description="Greets a user by name")
def greet_user(name):
return f"Hello, {name}!"

@register(name="square", description="Computes the square of a number")
def compute_square(n):
return n * n

@register(name="reverse", description="Reverses a string")
def reverse_string(s):
return s[::-1]

# List all registered functions
print("Registered functions:")
for name, entry in registry.items():
print(f" {name}: {entry['description']}")

print()

# Call registered functions by name
print(f"Calling 'greet': {registry['greet']['func']('Alice')}")
print(f"Calling 'square': {registry['square']['func'](5)}")
print(f"Calling 'reverse': {registry['reverse']['func']('abcd')}")

How it works: register() returns a decorator that captures the function object and stores it in the registry dict along with its metadata. The @register(...) syntax is sugar for greet_user = register(name="greet", ...)(greet_user).

Why this is important: This pattern is everywhere in production Python:

  • Flask/FastAPI: @app.route("/users") registers a handler function for a URL
  • Click: @cli.command() registers CLI subcommands
  • pytest: @pytest.fixture registers test fixtures
  • Celery: @app.task registers background tasks

All of these work because functions are first-class objects that can be stored, retrieved, and called dynamically.

# TODO: Build a function registry that stores functions and their metadata.
# Functions register themselves using a register() function.

registry = {}

def register(name=None, description="No description"):
    """Register a function in the global registry with optional metadata."""
    pass

# Register some functions
# ...

# List all registered functions
# ...

# Call a registered function by name
# ...
Expected Output
Registered functions:\n  greet: Greets a user by name\n  square: Computes the square of a number\n  reverse: Reverses a string\n\nCalling 'greet': Hello, Alice!\nCalling 'square': 25\nCalling 'reverse': dcba
Hints

Hint 1: register() should accept a function and store it in the registry dict along with its metadata.

Hint 2: You can use register() as a decorator or call it directly: register(name="greet", description="...")(greet_func).

Hint 3: Store both the function object and the description in the registry.


Hard

#10Object Inspector — MRO and AttributesHard
MROintrospectioninheritanceinspect

Build a comprehensive object inspector that takes any Python object and prints a detailed report: its type, identity, full Method Resolution Order (MRO), and all attributes categorized by kind (callable methods vs. data attributes, public vs. dunder vs. private).

# Test with a class that has multiple inheritance
class Animal:
species = "Unknown"
def speak(self):
return "..."

class Pet:
domesticated = True
def play(self):
return "Playing!"

class Dog(Animal, Pet):
breed = "Mixed"
def speak(self):
return "Woof!"
def fetch(self):
return "Fetching!"

rex = Dog()
rex.name = "Rex"

inspect_object(rex)
print("=" * 50)
inspect_object(Dog)
Solution
def inspect_object(obj):
"""Print a comprehensive inspection of any object."""
print(f"Object: {obj!r}")
print(f"Type: {type(obj).__name__}")
print(f"ID: {id(obj)}")

# Determine if obj is a class or an instance
is_class = isinstance(obj, type)
print(f"Is class: {is_class}")

# Method Resolution Order
if is_class:
mro = obj.__mro__
else:
mro = type(obj).__mro__
print(f"\nMRO ({len(mro)} classes):")
for i, cls in enumerate(mro):
marker = " <-- starts here" if i == 0 else ""
print(f" {i+1}. {cls.__name__}{marker}")

# Categorize attributes
public_methods = []
public_data = []
dunder_attrs = []
private_attrs = []

for name in sorted(dir(obj)):
try:
value = getattr(obj, name)
except AttributeError:
continue

is_callable = callable(value)

if name.startswith("__") and name.endswith("__"):
dunder_attrs.append((name, is_callable))
elif name.startswith("_"):
private_attrs.append((name, is_callable))
elif is_callable:
public_methods.append(name)
else:
public_data.append((name, repr(value)[:50]))

print(f"\nPublic methods ({len(public_methods)}):")
for name in public_methods:
print(f" .{name}()")

print(f"\nPublic data attributes ({len(public_data)}):")
for name, val in public_data:
print(f" .{name} = {val}")

print(f"\nDunder attributes: {len(dunder_attrs)} ({sum(1 for _, c in dunder_attrs if c)} callable)")
print(f"Private attributes: {len(private_attrs)}")


class Animal:
species = "Unknown"
def speak(self):
return "..."

class Pet:
domesticated = True
def play(self):
return "Playing!"

class Dog(Animal, Pet):
breed = "Mixed"
def speak(self):
return "Woof!"
def fetch(self):
return "Fetching!"

rex = Dog()
rex.name = "Rex"

inspect_object(rex)
print("=" * 50)
inspect_object(Dog)

What the MRO reveals: For Dog, the MRO is: Dog -> Animal -> Pet -> object. This is computed using the C3 linearization algorithm, which guarantees:

  1. Children come before parents
  2. Left parents come before right parents
  3. The order is consistent (no contradictions)

Why this is useful in production: Object inspectors like this are the basis of debugging tools (pdb), documentation generators (sphinx), serialization frameworks (dataclasses, pydantic), and ORM mappers (SQLAlchemy). Understanding how to introspect objects programmatically is essential for building frameworks.

def inspect_object(obj):
    """Print a comprehensive inspection of any object:
    - Type and identity
    - Method Resolution Order (for classes/instances)
    - All attributes grouped by category
    - Callable vs non-callable attributes
    """
    pass
Expected Output
See solution for full output
Hints

Hint 1: Use type(obj) for the type, id(obj) for identity.

Hint 2: For MRO, access type(obj).__mro__ (for instances) or obj.__mro__ (if obj is a class).

Hint 3: Use dir(obj) to get all attributes, getattr(obj, name) to get values, and callable() to check if something is callable.

#11Generic Type DispatcherHard
isinstancedispatchpolymorphismdesign-pattern

Build a TypeDispatcher that routes objects to different handler functions based on their type. It should support inheritance-aware dispatch: if there is no handler for the exact type, it walks the MRO to find a handler for a parent type.

# After implementing:
d = TypeDispatcher()
d.register(int, lambda x: x * 2)
d.register(str, lambda s: f"{s.upper()} reversed is {s.upper()[::-1]}")
d.register(list, lambda lst: f"sorted {sorted(lst)}")
d.register(object, lambda o: f"<fallback for {o}>") # Catch-all

print("Processing integer:", d.dispatch(42))
print("Processing string:", d.dispatch("hello"))
print("Processing list:", d.dispatch([3, 1, 8, 2, 5]))
print("Processing bool (matched int handler):", d.dispatch(True))
print("Processing float (matched object handler):", d.dispatch(3.14))
Solution
class TypeDispatcher:
"""A generic dispatcher that routes objects to handlers based on their type.
Supports inheritance — if no exact match, checks parent classes via MRO."""

def __init__(self):
self._handlers = {}

def register(self, type_or_types, handler):
"""Register a handler for one or more types."""
if isinstance(type_or_types, (list, tuple)):
for t in type_or_types:
self._handlers[t] = handler
else:
self._handlers[type_or_types] = handler

def dispatch(self, obj):
"""Find and call the appropriate handler for obj."""
# Walk the MRO — exact type is always first in __mro__
for cls in type(obj).__mro__:
if cls in self._handlers:
return self._handlers[cls](obj)
raise TypeError(f"No handler registered for {type(obj).__name__}")


d = TypeDispatcher()
d.register(int, lambda x: x * 2)
d.register(str, lambda s: f"{s.upper()} reversed is {s.upper()[::-1]}")
d.register(list, lambda lst: f"sorted {sorted(lst)}")
d.register(object, lambda o: f"<fallback for {o}>")

print("Processing integer:", d.dispatch(42))
print("Processing string:", d.dispatch("hello"))
print("Processing list:", d.dispatch([3, 1, 8, 2, 5]))
print("Processing bool (matched int handler):", d.dispatch(True))
print("Processing float (matched object handler):", d.dispatch(3.14))

Key behaviors:

  • dispatch(True): bool.__mro__ is (bool, int, object). No handler for bool, but there is one for int, so it matches. True is treated as integer 1, so 1 * 2 = 2.
  • dispatch(3.14): float.__mro__ is (float, object). No handler for float, falls through to the object handler.

Real-world equivalents:

  • Python's own functools.singledispatch uses exactly this pattern
  • json.JSONEncoder.default() dispatches serialization by type
  • Web frameworks route request handling based on content type
  • ORMs map Python types to database column types

Why walking the MRO matters: Without MRO-based fallback, you would need to register handlers for every single type. With it, you can register handlers for base types and let subclasses inherit the behavior — just like method resolution in normal inheritance.

class TypeDispatcher:
    """A generic dispatcher that routes objects to handlers based on their type.
    Supports inheritance — if no exact match, checks parent classes via MRO."""

    def __init__(self):
        self._handlers = {}

    def register(self, type_or_types, handler):
        """Register a handler for one or more types."""
        pass

    def dispatch(self, obj):
        """Find and call the appropriate handler for obj.
        Check exact type first, then walk the MRO for parent matches.
        Raise TypeError if no handler is found."""
        pass
Expected Output
Processing integer: 84\nProcessing string: HELLO reversed is OLLEH\nProcessing list: sorted [1, 2, 3, 5, 8]\nProcessing bool (matched int handler): 2\nProcessing float (matched object handler): <fallback for 3.14>
Hints

Hint 1: Store handlers keyed by type: self._handlers[some_type] = handler.

Hint 2: For dispatch, first check type(obj) for an exact match in _handlers.

Hint 3: If no exact match, walk type(obj).__mro__ to find the closest parent type with a handler.

#12Plugin Loader — Modules as First-Class ObjectsHard
modulesimportlibpluginsintrospection

Build a plugin loader that dynamically creates module objects, attaches functions to them, and provides a system for discovering and calling plugin functions. This demonstrates that modules are first-class objects you can create, manipulate, and introspect at runtime.

# After implementing:
loader = PluginLoader()

# Register plugins by providing a dict of functions
loader.create_plugin("math_utils", {
"double": lambda x: x * 2,
"square": lambda x: x ** 2,
})

loader.create_plugin("string_utils", {
"upper": lambda s: s.upper(),
"repeat": lambda s, n: s * n,
})

# List all plugins
print("Loaded plugins:", loader.list_plugins())

# Introspect a plugin
info = loader.get_plugin_info("math_utils")
print(f"\nPlugin: {info['name']}")
print(f" Functions: {info['functions']}")
print(f" Module type: {info['type']}")
print(f" Is object: {info['is_object']}")

# Call plugin functions
print(f"\ndouble(5) = {loader.call('math_utils', 'double', 5)}")
print(f"square(5) = {loader.call('math_utils', 'square', 5)}")
print(f"upper('hello') = {loader.call('string_utils', 'upper', 'hello')}")
print(f"repeat('ab', 3) = {loader.call('string_utils', 'repeat', 'ab', 3)}")
Solution
import types
import sys

class PluginLoader:
"""A plugin system that treats modules as first-class objects."""

def __init__(self):
self._plugins = {}

def create_plugin(self, name, functions_dict):
"""Create a plugin (module object) from a dict of name->function mappings."""
# Create a real module object dynamically
module = types.ModuleType(name)
module.__doc__ = f"Plugin module: {name}"

# Attach each function as a module attribute
for func_name, func in functions_dict.items():
# Give the function a proper name if it's a lambda
if hasattr(func, '__name__') and func.__name__ == '<lambda>':
func.__name__ = func_name
func.__qualname__ = f"{name}.{func_name}"
setattr(module, func_name, func)

self._plugins[name] = module

# Optionally register in sys.modules so import works
sys.modules[f"plugins.{name}"] = module

return module

def list_plugins(self):
"""Return names of all loaded plugins."""
return list(self._plugins.keys())

def call(self, plugin_name, func_name, *args, **kwargs):
"""Call a function from a named plugin."""
if plugin_name not in self._plugins:
raise KeyError(f"Plugin '{plugin_name}' not found")
module = self._plugins[plugin_name]
func = getattr(module, func_name, None)
if func is None or not callable(func):
raise AttributeError(
f"Plugin '{plugin_name}' has no callable '{func_name}'"
)
return func(*args, **kwargs)

def get_plugin_info(self, plugin_name):
"""Return detailed introspection of a plugin module."""
if plugin_name not in self._plugins:
raise KeyError(f"Plugin '{plugin_name}' not found")
module = self._plugins[plugin_name]
functions = [
name for name in dir(module)
if not name.startswith("_") and callable(getattr(module, name))
]
return {
"name": plugin_name,
"type": type(module),
"is_object": isinstance(module, object),
"functions": sorted(functions),
"doc": module.__doc__,
}


loader = PluginLoader()

loader.create_plugin("math_utils", {
"double": lambda x: x * 2,
"square": lambda x: x ** 2,
})

loader.create_plugin("string_utils", {
"upper": lambda s: s.upper(),
"repeat": lambda s, n: s * n,
})

print("Loaded plugins:", loader.list_plugins())

info = loader.get_plugin_info("math_utils")
print(f"\nPlugin: {info['name']}")
print(f" Functions: {info['functions']}")
print(f" Module type: {info['type']}")
print(f" Is object: {info['is_object']}")

print(f"\ndouble(5) = {loader.call('math_utils', 'double', 5)}")
print(f"square(5) = {loader.call('math_utils', 'square', 5)}")
print(f"upper('hello') = {loader.call('string_utils', 'upper', 'hello')}")
print(f"repeat('ab', 3) = {loader.call('string_utils', 'repeat', 'ab', 3)}")

How it works:

  1. types.ModuleType(name) creates a real module object — the same kind of object that import produces.
  2. setattr(module, func_name, func) attaches functions as module attributes — exactly like how Python sets up attributes when importing a .py file.
  3. sys.modules[f"plugins.{name}"] = module optionally registers the module so that from plugins.math_utils import double would work.

Real-world applications:

  • pytest discovers and loads test plugins by scanning for module objects with specific attributes
  • Django loads app configurations by importing module objects and inspecting their contents
  • Jupyter creates synthetic modules when running notebook cells
  • Mock libraries create fake modules using types.ModuleType for testing

Key takeaway: Modules are not special — they are regular Python objects of type module. You can create them programmatically, attach any attributes to them, pass them around, store them in collections, and introspect them with dir(), getattr(), and vars(). This is the ultimate proof that everything in Python truly is an object.

import types
import sys

class PluginLoader:
    """A plugin system that treats modules as first-class objects.
    Dynamically creates module objects, registers plugin functions,
    and provides discovery and execution."""

    def __init__(self):
        self._plugins = {}  # name -> module object

    def create_plugin(self, name, functions_dict):
        """Create a plugin (module object) from a dict of name->function mappings."""
        pass

    def list_plugins(self):
        """Return info about all loaded plugins."""
        pass

    def call(self, plugin_name, func_name, *args, **kwargs):
        """Call a function from a named plugin."""
        pass

    def get_plugin_info(self, plugin_name):
        """Return detailed introspection of a plugin module."""
        pass
Expected Output
Loaded plugins: ['math_utils', 'string_utils']\n\nPlugin: math_utils\n  Functions: ['double', 'square']\n  Module type: <class 'module'>\n  Is object: True\n\ndouble(5) = 10\nsquare(5) = 25\nupper('hello') = HELLO\nrepeat('ab', 3) = ababab
Hints

Hint 1: Use types.ModuleType(name) to create a module object dynamically.

Hint 2: Set functions as attributes on the module: setattr(module, func_name, func).

Hint 3: Use dir() or vars() to discover what functions a plugin module contains.

© 2026 EngineersOfAI. All rights reserved.