Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
247: The inspect Module for Introspection
Every now and then, you'll find yourself building something that needs to be "smart" about the code it's running. Maybe you're building a plugin system, a custom test runner, or a sophisticated decorator that needs to validate its target. This is where introspection comes in—the ability of a Python program to examine its own objects at runtime.
When I first started doing this, I took the path of least resistance. I knew that functions in Python were objects, and those objects had attributes. I figured I could just poke around the internals to find what I needed.
The temptation of the dunder attributes
Let's say we're building a simple task registry. We want to ensure that any function registered as a "task" accepts exactly two arguments: data and context. If it doesn't, we want to throw an error immediately upon registration rather than waiting for the task to fail mid-execution.
The naive approach is to dive straight into the __code__ object. It feels efficient because you're touching the raw data:
def register_task(func):
# Naive check: looking at the variable names in the code object
args = func.__code__.co_varnames
if args != ('data', 'context'):
raise TypeError(f"Task {func.__name__} must accept exactly 'data' and 'context'")
print(f"Registered {func.__name__} successfully!")
def my_task(data, context):
pass
register_task(my_task) # This works... for now.
This looks fine until you realize how brittle this is. The co_varnames tuple doesn't just contain the arguments; it contains all local variables defined within the function body. If you add a single temporary variable inside my_task, your registration logic suddenly breaks. Even worse, you're relying on CPython implementation details. If you ever ran this on PyPy or another implementation, you might find that these attributes behave differently or don't exist at all.
The elegance of the inspect module
Instead of digging through the plumbing, you should use the inspect module. It provides a high-level, standardized API for introspection that handles the edge cases for you. The real star of the show here is inspect.signature().
If we rewrite our registry using inspect, the code becomes not only more robust but actually more readable:
import inspect
def register_task(func):
sig = inspect.signature(func)
params = list(sig.parameters.keys())
if params != ['data', 'context']:
raise TypeError(f"Task {func.__name__} must accept exactly 'data' and 'context'")
print(f"Registered {func.__name__} successfully!")
def my_task(data, context):
temp_var = 10 # This no longer breaks the registration!
return f"Processed {data}"
register_task(my_task)
By using inspect.signature, we get a Signature object. This object doesn't care about local variables; it only cares about the function's interface. It understands the difference between positional arguments, keyword-only arguments, and variable arguments (like *args or **kwargs).
When the internals lie to you
You might wonder why we don't just use dir() or type(). While those are great for quick debugging in a REPL, they give you a flat list of attributes. They don't tell you the intent of the code. For example, if you wanted to know if a function has a default value for a specific argument, trying to find that in __defaults__ is a nightmare of indexing and offset calculations.
With inspect, you can simply iterate over the parameters and check the default attribute of each Parameter object. It turns a complex piece of reverse-engineering into a simple property check. I'll admit, the inspect module is slightly slower than hitting __code__ directly, but in the context of registration or setup logic, that overhead is negligible compared to the cost of a production bug caused by a local variable change.
📋 Practical Task
Build an Automated Dependency Injector
Your task is to create a function called inject_dependencies(func, deps). This function should act as a basic dependency injector.
Requirements:
- The
inject_dependenciesfunction should take a target functionfuncand a dictionarydepscontaining available dependencies (e.g.,{'db': database_connection, 'logger': logger_instance}). - Use the
inspectmodule to determine which arguments the target functionfuncactually requires. - The injector should only pass arguments from the
depsdictionary that match the parameter names of the function. - If the function requires an argument that is not present in the
depsdictionary, it should raise aRuntimeErrorexplaining which dependency is missing.
Example Scenario:
def save_user(user_id, db, logger):
logger.log(f"Saving user {user_id}")
db.save(user_id)
dependencies = {'db': 'DB_CONNECTION_OBJ', 'logger': 'LOGGER_OBJ'}
# This should work if you provide the 'user_id' separately or handle it
# For the exercise, ensure your injector can call the function
# by merging the provided 'deps' with any other necessary arguments.
There are no comments for now.