Python Everything Is an Object Practice Problems & Exercises
Practice: Everything Is an Object
← Back to lessonEasy
Prove that functions in Python are full objects: they have a type, can be assigned to variables, and carry attributes.
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.
Predict the output, then run to verify. What is the type of type itself?
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'>—typeis 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 istypeall the way down.type is type(type)isTrue—typeand its own type are the same object.isinstance(type, type)isTrue—typeis an instance oftype.
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
TrueHints
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.
Demonstrate that imported modules are regular Python objects with a type and attributes.
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__ attributesExpected Output
<class 'module'>
math
TrueHints
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.
Verify that every value in Python is an instance of object — integers, strings, None, functions, classes, and even object itself.
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 resultExpected 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): TrueHints
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
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."""
passExpected 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.
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."""
passExpected Output
Hello, Alice! (call #1)\nHello, Bob! (call #2)\nHello, Charlie! (call #3)\nTotal calls: 3Hints
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.
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
42has 68+ dunder methods —__add__,__mul__,__eq__,__hash__, etc. These are the methods that make operators work:42 + 8is 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."""
passExpected 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 attributesHints
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.
Predict the output. Classes themselves are objects — what type are they?
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 oftype. Thetypeobject is the metaclass: the class that creates classes. isinstance(Dog, type)isTruebecauseDogis an object created bytype.- 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.
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.fixtureregisters test fixtures - Celery:
@app.taskregisters 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': dcbaHints
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
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:
- Children come before parents
- Left parents come before right parents
- 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
"""
passExpected Output
See solution for full outputHints
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.
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 forbool, but there is one forint, so it matches.Trueis treated as integer1, so1 * 2 = 2.dispatch(3.14):float.__mro__is(float, object). No handler forfloat, falls through to theobjecthandler.
Real-world equivalents:
- Python's own
functools.singledispatchuses 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."""
passExpected 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.
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:
types.ModuleType(name)creates a real module object — the same kind of object thatimportproduces.setattr(module, func_name, func)attaches functions as module attributes — exactly like how Python sets up attributes when importing a.pyfile.sys.modules[f"plugins.{name}"] = moduleoptionally registers the module so thatfrom plugins.math_utils import doublewould 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.ModuleTypefor 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."""
passExpected 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) = abababHints
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.
