Code Introspection & Metadata
Inspect live objects, docstrings, annotations, and module namespaces at runtime using built-in reflection.
The Python X-Ray Machine Mental Model
Think of Python as an x-ray machine for its own code. In rigid compiled languages, code is frozen into opaque machine instructions, and variable names, documentation, and types disappear after compilation. In Python, everything is a living, queryable object. A function carries its own birth certificate (__name__), its instruction manual (__doc__), its architectural blueprint (__annotations__), and its compiled bytecode machinery (__code__). Introspection (or reflection) is the superpower that allows programs to examine their own components at runtime, dynamically discover capabilities, validate schemas, and build extensible plugin architectures.
02 / Core Introspection Built-in Functions
Python provides five fundamental introspection built-ins that allow code to query types, test invocation capability, and dynamically inspect attributes safely.
type() checks exact class identity. isinstance() traverses the Method Resolution Order (MRO), respecting inheritance and polymorphism.
Tests if an object implements __call__. Returns True for functions, methods, classes, and instances with a call method.
Dynamically retrieves attribute value by string name with safe fallback default, preventing unhandled AttributeError exceptions.
03 / CPython PyObject Memory Structure & Attribute Lookup
Under the hood, introspection relies on CPython's uniform PyObject header and attribute lookup pipelines:
Every Python object begins with ob_refcnt and ob_type pointer. Calling type(obj) simply dereferences the ob_type pointer directly to return its PyTypeObject.
isinstance(obj, Class) scans the object's class tuple in __mro__ (Method Resolution Order). If Class appears anywhere in the hierarchy, it returns True.
getattr() invokes PyObject_GetAttr: checks class descriptors, checks instance __dict__, falls back to __getattr__, and returns fallback default on AttributeError.
Function annotations are evaluated at compile time and stored in __annotations__. CPython performs zero runtime type enforcement, incurring 0ns CPU overhead during execution.
04 / Introspection Built-ins Comparison Matrix
| Function | Syntax | Primary Purpose | Return Type | Behavior on Failure |
|---|---|---|---|---|
| type() | type(obj) | Exact class identification | type | Always returns type |
| isinstance() | isinstance(x, (A, B)) | Inheritance & subclass validation | bool | Returns False |
| callable() | callable(obj) | Invocation capability check | bool | Returns False |
| hasattr() | hasattr(obj, 'attr') | Attribute existence query | bool | Catches AttributeError → False |
| getattr() | getattr(obj, 'attr', default) | Dynamic attribute retrieval | Any | Returns default (or raises) |
05 / Official Syntax & Machinery Reference
Returns the exact class of the object
Return: typeTests whether object is an instance of a class or subclass
Return: boolTests whether object implements __call__
Return: boolTests if attribute exists on object
Return: boolDynamically retrieves attribute value
Return: AnyDynamically sets attribute value on object
Return: NoneLists all attribute and method names for an object
Return: list[str]06 / Tiered Code Walkthroughs & Worked Examples
Core Introspection Built-ins: type(), isinstance(), and callable()
Verifying runtime types, checking inheritance hierarchies, and testing invocation capability.
class Service:
def run(self):
return 'Service running'
class AuthService(Service):
pass
auth = AuthService()
# 1. type() checks exact class (ignores inheritance)
print('type(auth) is AuthService?', type(auth) is AuthService)
print('type(auth) is Service?', type(auth) is Service) # False!
# 2. isinstance() respects class inheritance
print('isinstance(auth, Service)?', isinstance(auth, Service)) # True!
print('isinstance(auth, (int, Service))?', isinstance(auth, (int, Service))) # Tuple check
# 3. callable() tests if an object can be invoked
print('Is auth callable?', callable(auth))
print('Is auth.run callable?', callable(auth.run))
print('Is AuthService class callable?', callable(AuthService)) # Classes are callable!
Dynamic Attribute Access: hasattr(), getattr(), and setattr()
Inspecting, retrieving, and setting attributes dynamically at runtime with safe default fallbacks.
class ServerConfig:
host = '127.0.0.1'
port = 8080
cfg = ServerConfig()
# Checking attribute existence
print('Has host?', hasattr(cfg, 'host'))
print('Has timeout?', hasattr(cfg, 'timeout'))
# Safely retrieving attributes with fallback defaults
timeout = getattr(cfg, 'timeout', 30)
print('Resolved timeout:', timeout)
# Setting dynamic attributes at runtime
setattr(cfg, 'environment', 'production')
print('Dynamically set environment:', getattr(cfg, 'environment'))
# Safe attribute cleanup
delattr(cfg, 'environment')
print('Still has environment?', hasattr(cfg, 'environment'))
Inspecting Function Metadata (__name__, __doc__, __code__)
Extracting runtime metadata and internal properties directly from Python function objects.
def process_transaction(sender: str, recipient: str, amount: float = 0.0) -> bool:
"""Transfer funds between two verified customer accounts."""
return amount > 0
print('Function name (__name__):', process_transaction.__name__)
print('Module origin (__module__):', process_transaction.__module__)
print('Docstring (__doc__):', process_transaction.__doc__.strip())
# Inspecting the underlying code object
co = process_transaction.__code__
print('Positional parameter count:', co.co_argcount)
print('Local variable names:', co.co_varnames)
print('Defined in file:', co.co_filename.split('\\')[-1])
Type Hints and the __annotations__ Dictionary
Examining function type annotations at runtime for schema introspection.
def fetch_records(query: str, limit: int = 100, active_only: bool = True) -> list:
"""Query database for records matching criteria."""
return []
annotations = fetch_records.__annotations__
print('Raw __annotations__ mapping:', annotations)
print('Parameter "query" expected type:', annotations['query'].__name__)
print('Return expected type:', annotations['return'].__name__)
# Notice: Python does NOT enforce these types at runtime automatically!
result = fetch_records(12345) # Calling with int instead of str executes without error
print('Executed without type enforcement:', result)
Deep Parameter Inspection Using the inspect Module
Using inspect.signature() to introspect parameter kinds, defaults, and type annotations.
import inspect
def endpoint_handler(request_id: str, timeout: int = 30, *tags, verbose: bool = False, **extra):
pass
sig = inspect.signature(endpoint_handler)
print('Full signature:', sig)
print('Parameters breakdown:')
for name, param in sig.parameters.items():
print(f' {name:12} | Kind: {param.kind.name:18} | Default: {param.default} | Type: {param.annotation}')
Dynamic Plugin / CLI Command Dispatcher with getattr()
Building an extensible CLI command processor that discovers and routes actions dynamically.
class ServiceManager:
def do_start(self, service_name):
return f'Service "{service_name}" started successfully.'
def do_stop(self, service_name):
return f'Service "{service_name}" stopped.'
def execute(self, action, service_name):
method_name = f'do_{action}'
# Introspect whether handler method exists on self
if not hasattr(self, method_name):
return f'Unknown action: "{action}"'
handler = getattr(self, method_name)
return handler(service_name)
manager = ServiceManager()
print(manager.execute('start', 'auth-worker'))
print(manager.execute('stop', 'auth-worker'))
print(manager.execute('restart', 'auth-worker')) # Unrecognized
Building a Runtime Type-Enforcing Decorator
Leveraging __annotations__ and inspect to validate argument types at runtime.
import functools, inspect
def enforce_types(func):
sig = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name, value in bound.arguments.items():
expected = sig.parameters[name].annotation
if expected is not inspect.Parameter.empty and not isinstance(value, expected):
raise TypeError(f'Argument "{name}" must be {expected.__name__}, got {type(value).__name__}')
return func(*args, **kwargs)
return wrapper
@enforce_types
def send_alert(message: str, code: int):
return f'[{code}] {message}'
print(send_alert('Disk space low', 500))
try:
send_alert('Disk space low', 'FIVE_HUNDRED') # Passing str for int
except TypeError as err:
print('Type validation failed:', err)
07 / Live Python Reflection Workbench
Select different Python constructs (Function, Class, Instance, Built-in C function) to examine their runtime descriptors, metadata, and bytecode properties.
08 / Dynamic Plugin Command Dispatcher (getattr in Action)
Modern Python systems (CLI tools, bot frameworks, test runners) use hasattr() and getattr() to dynamically discover and invoke handlers by string name.
09 / Common Anti-Patterns & Pitfalls
type(x) == cls checks for strict equality with the exact class. If x is an instance of a subclass, type(x) == cls evaluates to False, breaking polymorphic code and inheritance. Always use isinstance(x, cls).
class Base: pass
class Derived(Base): pass
obj = Derived()
# Flawed: fails on subclasses!
if type(obj) == Base:
print('Base instance')
else:
print('Rejected derived class!')
class Base: pass
class Derived(Base): pass
obj = Derived()
# Correct: respects inheritance hierarchy
if isinstance(obj, Base):
print('Valid Base instance or subclass')
getattr, hasattr, and setattr are built-in functions, NOT methods on objects. Writing obj.getattr('key') raises AttributeError: '...' object has no attribute 'getattr'.
class User:
name = 'Alex'
u = User()
try:
val = u.getattr('name') # Raises AttributeError
except AttributeError as e:
print('Method error:', e)
class User:
name = 'Alex'
u = User()
# Correct: top-level built-in function
val = getattr(u, 'name')
print('Retrieved:', val)
Calling getattr(obj, name) without a third argument raises AttributeError if the attribute does not exist. Always supply a sensible fallback default (e.g. getattr(obj, name, None)).
class Settings:
theme = 'dark'
s = Settings()
try:
lang = getattr(s, 'language') # Raises AttributeError
except AttributeError:
print('Language missing!')
class Settings:
theme = 'dark'
s = Settings()
lang = getattr(s, 'language', 'en-US') # Safe fallback
print('Language:', lang)
Python's type hints (e.g. def f(x: int)) are strictly informational metadata stored in __annotations__. CPython does not validate or reject incorrect types at runtime without third-party libraries (like Pydantic or Beartype).
def square(x: int) -> int:
return x * x
# Python does NOT crash; passes string and crashes inside arithmetic!
try:
square('hello') # TypeError: can't multiply sequence by non-int
except TypeError as e:
print('Runtime error:', e)
def square(x: int) -> int:
if not isinstance(x, int):
raise TypeError(f'Expected int, got {type(x).__name__}')
return x * x
10 / Compiler Edge Cases & Quirks
In Python, classes are themselves instances of type and implement __call__ to instantiate objects. Therefore, callable(MyClass) returns True.
class Factory:
pass
print('Is Factory class callable?', callable(Factory))
f_instance = Factory()
print('Is instance callable?', callable(f_instance))
Functions implemented in C (such as len, sum, print) are instances of builtin_function_or_method. They do NOT have a __code__ attribute because their implementation is compiled native C, not Python bytecode.
import sys
print('Does len have __code__?', hasattr(len, '__code__'))
def py_func(): pass
print('Does py_func have __code__?', hasattr(py_func, '__code__'))
Classes can implement a custom __dir__(self) method to control what attributes are displayed when dir(instance) is called, useful in dynamic proxies and API wrappers.
class DynamicProxy:
def __dir__(self):
return ['virtual_field_1', 'virtual_field_2']
proxy = DynamicProxy()
print('Custom dir:', dir(proxy))
When using future annotations (from __future__ import annotations) or quotes ('Node'), annotations are stored as strings rather than type objects. Introspection libraries use typing.get_type_hints() to resolve them.
def link(next_node: 'Node') -> None:
pass
print('String annotation:', link.__annotations__['next_node'])
11 / Execution Performance & Memory Tradeoffs
getattr(obj, 'name') requires a string hash lookup in the class/instance __dict__. It is slightly slower than direct dot access (obj.name), which CPython optimizes via inline attribute caching in Python 3.12.
Calling inspect.signature() on every function call in a hot loop is expensive because it constructs full Signature and Parameter objects. Cache signatures on startup if performing runtime dependency injection.
Type hints carry zero runtime CPU cost during execution; they are stored in a static dictionary on the function object and evaluated only if introspected.
dir(obj) allocates and sorts a list of strings containing all attributes and MRO methods. Avoid calling dir() in high-throughput production request paths.
12 / Engineering Workflow: Building Dynamic Architectures
Never call getattr(obj, name) without a default fallback unless you explicitly want an unhandled AttributeError to crash.
inspect.signature() is computationally expensive. Cache parsed parameter signatures on startup if building an API router or dependency injector.
Use type hints for static analysis with mypy and IDE autocompletion rather than writing manual runtime isinstance() checks on every argument.
13 / Real-World Production Architecture
Modern Web Frameworks & Dependency Injection (FastAPI / Pydantic)
Modern API frameworks inspect endpoint function signatures using inspect.signature() and __annotations__, automatically extracting query parameters, headers, and request bodies from HTTP requests and validating them against schemas.
Extensible Plugin & Extension Architectures
Applications (like pytest or custom test runners) discover test functions and plugin hooks dynamically using hasattr() and getattr(), executing matching callables without hardcoding imports.
Automated Serialization & ORM Mapping (SQLAlchemy / Django)
Object-relational mappers inspect model class attributes and type hints at startup, generating database table schemas and SQL migrations automatically.
14 / Instant Self-Check Quiz Booster
x = 42 print(isinstance(x, int))
msg = "hello" print(hasattr(msg, "upper"))
class Box:
size = 10
b = Box()
print(getattr(b, "size", 0))def greet(): pass print(callable(greet))
class Point: pass p = Point() setattr(p, "x", 5) print(getattr(p, "x"))
15 / Guided Practice Labs & Test Suites
Runtime Parameter Type Validator
Write a function validate_arguments(func, **kwargs) that inspects func.__annotations__ and verifies that each argument in kwargs matches the expected type. Return True if all provided arguments match their annotated types (or have no annotation), and False if any argument violates its type annotation.
def validate_arguments(func, **kwargs):
# Check kwargs against func.__annotations__
pass
def sample(name: str, count: int):
pass
assert validate_arguments(sample, name='hello', count=3) is True
assert validate_arguments(sample, name='hello', count='invalid') is False
assert validate_arguments(sample, unknown=42) is True
Filter Public Callable Methods on Any Object
Write a function list_public_methods(obj) that uses dir(), getattr(), and callable() to return a sorted list of all public method names (attributes that are callable and do NOT start with '_').
def list_public_methods(obj):
# Return sorted list of callable public method names
pass
class Sample:
data = 10
def run(self): pass
def stop(self): pass
def _internal(self): pass
s = Sample()
assert list_public_methods(s) == ['run', 'stop']
Batch Attribute Extractor with Fallbacks
Write a function pluck_attributes(obj, attribute_map) where attribute_map is a dictionary mapping attribute names to default fallback values. Return a dictionary mapping each attribute name to its extracted value on obj, or the fallback default if the attribute is absent.
def pluck_attributes(obj, attribute_map):
# Extract attributes using getattr with fallback defaults
pass
class Config:
host = 'localhost'
port = 8080
c = Config()
res = pluck_attributes(c, {'host': '127.0.0.1', 'port': 80, 'debug': False})
assert res == {'host': 'localhost', 'port': 8080, 'debug': False}
16 / Production Debugging Lab
A payment gateway rejected CustomBillingRequest instances even though it inherited from BillingRequest.A worker crashed with AttributeError: 'Server' object has no attribute 'max_connections' during startup.17 / Key Takeaways & Module 05 Milestone Achieved
- Introspection is Python's ability to inspect object types, attributes, and docstrings at runtime.
- type() returns exact class; isinstance() respects inheritance across the MRO.
- hasattr(), getattr(), and setattr() enable safe dynamic attribute discovery and dispatch.
- Type hints populate the __annotations__ dictionary without imposing runtime performance penalties.
- The inspect module provides rich signature parsing for building dynamic frameworks and dependency injection systems.
Functions & Scopes Mastered!
You have completed all 6 lessons in Functions & Scopes, mastering first-class functions, parameter mechanics, variadic arguments (*args & **kwargs), lexical closures, CPython stack frame recursion, and runtime object introspection.
Worked example
Inspect a function's name and documentation at runtime.
def ping(): """Send ping packet.""" pass
print(f"{ping.__name__}: {ping.__doc__.strip()}")
How it works, step by step
- Functions store their identifier in __name__ and docstring in __doc__.
Code Introspection & Metadata practice
Write a function describe(func) that takes a function object and prints Function '{func.__name__}' is callable: {callable(func)}. Test with a function demo().
Coding Practice is Locked
Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.
Practice quiz
Check yourself
15 questions with explanations after submission, so you can correct course as you go.
Code Introspection & Metadata quiz
LockedYou must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.
Lesson incomplete
Not completedCreate an ID or sign in to save lesson completion across devices.