Skip to contentSkip to navigation
Module 05 • Lesson 06 (Capstone)intermediate 20 mins read

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.

__name__
Birth Certificate
__doc__
Instruction Manual
__annotations__
Type Schema Blueprint
__code__
Compiled Bytecode

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(x) & isinstance(x, cls)

type() checks exact class identity. isinstance() traverses the Method Resolution Order (MRO), respecting inheritance and polymorphism.

callable(obj)

Tests if an object implements __call__. Returns True for functions, methods, classes, and instances with a call method.

getattr(obj, name, default)

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:

1. PyObject Header & ob_type

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.

2. MRO Traversal (isinstance)

isinstance(obj, Class) scans the object's class tuple in __mro__ (Method Resolution Order). If Class appears anywhere in the hierarchy, it returns True.

3. PyObject_GetAttr Pipeline

getattr() invokes PyObject_GetAttr: checks class descriptors, checks instance __dict__, falls back to __getattr__, and returns fallback default on AttributeError.

4. Zero-Cost Runtime Annotations

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

FunctionSyntaxPrimary PurposeReturn TypeBehavior on Failure
type()type(obj)Exact class identificationtypeAlways returns type
isinstance()isinstance(x, (A, B))Inheritance & subclass validationboolReturns False
callable()callable(obj)Invocation capability checkboolReturns False
hasattr()hasattr(obj, 'attr')Attribute existence queryboolCatches AttributeError → False
getattr()getattr(obj, 'attr', default)Dynamic attribute retrievalAnyReturns default (or raises)

05 / Official Syntax & Machinery Reference

type(obj)type(x)

Returns the exact class of the object

Notes: Direct inspection; does not check inheritance
Return: type
isinstance()isinstance(x, (T1, T2))

Tests whether object is an instance of a class or subclass

Notes: Accepts single class or tuple of classes
Return: bool
callable()callable(x)

Tests whether object implements __call__

Notes: Returns True for functions, methods, classes, callable instances
Return: bool
hasattr()hasattr(obj, 'attr')

Tests if attribute exists on object

Notes: Calls getattr internally; catches AttributeError
Return: bool
getattr()getattr(obj, 'attr', default)

Dynamically retrieves attribute value

Notes: Returns default if provided; raises AttributeError if omitted
Return: Any
setattr()setattr(obj, 'attr', val)

Dynamically sets attribute value on object

Notes: Equivalent to obj.attr = val
Return: None
dir()dir(obj)

Lists all attribute and method names for an object

Notes: Returns sorted list of string names
Return: list[str]

06 / Tiered Code Walkthroughs & Worked Examples

Core Introspection Built-ins: type(), isinstance(), and callable()

Core

Verifying runtime types, checking inheritance hierarchies, and testing invocation capability.

Core Introspection Built-ins: type(), isinstance(), and callable() Run cleanly to earn +1 XP
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!
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

Dynamic Attribute Access: hasattr(), getattr(), and setattr()

Core

Inspecting, retrieving, and setting attributes dynamically at runtime with safe default fallbacks.

Dynamic Attribute Access: hasattr(), getattr(), and setattr() Run cleanly to earn +1 XP
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'))
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

Inspecting Function Metadata (__name__, __doc__, __code__)

Core

Extracting runtime metadata and internal properties directly from Python function objects.

Inspecting Function Metadata (__name__, __doc__, __code__) Run cleanly to earn +1 XP
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])
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

Type Hints and the __annotations__ Dictionary

Core

Examining function type annotations at runtime for schema introspection.

Type Hints and the __annotations__ Dictionary Run cleanly to earn +1 XP
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)
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

Deep Parameter Inspection Using the inspect Module

Core

Using inspect.signature() to introspect parameter kinds, defaults, and type annotations.

Deep Parameter Inspection Using the inspect Module Run cleanly to earn +1 XP
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}')
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

Dynamic Plugin / CLI Command Dispatcher with getattr()

Core

Building an extensible CLI command processor that discovers and routes actions dynamically.

Dynamic Plugin / CLI Command Dispatcher with getattr() Run cleanly to earn +1 XP
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
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

Building a Runtime Type-Enforcing Decorator

Core

Leveraging __annotations__ and inspect to validate argument types at runtime.

Building a Runtime Type-Enforcing Decorator Run cleanly to earn +1 XP
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)
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

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.

Select Query Target:
TARGET: process_payment(sender, amount, currency='USD') -> bool
type(target): <class 'function'>
callable(target): True
target.__name__: process_payment
target.__module__: __main__
target.__doc__: "Debit sender account and credit merchant vault with atomic locks."
hasattr(target, '__code__'): True (Python Bytecode)
target.__annotations__: {'sender': <class 'str'>, 'amount': <class 'float'>, 'currency': <class 'str'>, 'return': <class 'bool'>}
__code__.co_varnames: ('sender', 'amount', 'currency', 'tx_id')

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.

Dispatch Telemetry Log
Click “Dispatch via getattr()” to simulate dynamic routing.

09 / Common Anti-Patterns & Pitfalls

🟢 Beginner Pitfall: Using type(x) == cls Instead of isinstance(x, cls)

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).

Flawed Implementation
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!')
Correct Pattern
class Base: pass
class Derived(Base): pass
obj = Derived()
# Correct: respects inheritance hierarchy
if isinstance(obj, Base):
    print('Valid Base instance or subclass')
🟢 Beginner Pitfall: Calling getattr() as an Object Method (obj.getattr())

getattr, hasattr, and setattr are built-in functions, NOT methods on objects. Writing obj.getattr('key') raises AttributeError: '...' object has no attribute 'getattr'.

Flawed Implementation
class User:
    name = 'Alex'
u = User()
try:
    val = u.getattr('name')  # Raises AttributeError
except AttributeError as e:
    print('Method error:', e)
Correct Pattern
class User:
    name = 'Alex'
u = User()
# Correct: top-level built-in function
val = getattr(u, 'name')
print('Retrieved:', val)
🟡 Intermediate Pitfall: Omitting Default Fallback in getattr() on Optional Attributes

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)).

Flawed Implementation
class Settings:
    theme = 'dark'
s = Settings()
try:
    lang = getattr(s, 'language')  # Raises AttributeError
except AttributeError:
    print('Language missing!')
Correct Pattern
class Settings:
    theme = 'dark'
s = Settings()
lang = getattr(s, 'language', 'en-US')  # Safe fallback
print('Language:', lang)
🟡 Intermediate Pitfall: Assuming Python Enforces Type Annotations at Runtime

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).

Flawed Implementation
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)
Correct Pattern
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

Classes are Callable Objects

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))
Output: Is Factory class callable? True Is instance callable? False
Built-in C Functions Lacking __code__

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__'))
Output: Does len have __code__? False Does py_func have __code__? True
Customizing dir() with __dir__()

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))
Output: Custom dir: ['virtual_field_1', 'virtual_field_2']
Forward Reference Type Hints as Strings

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'])
Output: String annotation: Node

11 / Execution Performance & Memory Tradeoffs

getattr overhead

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.

inspect signature cost

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.

zero cost annotations

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 allocation

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

Rule 1: Always Supply Defaults

Never call getattr(obj, name) without a default fallback unless you explicitly want an unhandled AttributeError to crash.

Rule 2: Cache Signatures

inspect.signature() is computationally expensive. Cache parsed parameter signatures on startup if building an API router or dependency injector.

Rule 3: Trust Type Checkers

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

Framework Engineering

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.

Framework Engineering

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.

Framework Engineering

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

17 Questions
Question 1 of 17 • easy
What does the type() built-in function return?
Question 2 of 17 • easy
What does this print?
x = 42
print(isinstance(x, int))
Question 3 of 17 • easy
What does dir(obj) return?
Question 4 of 17 • easy
What does this print?
msg = "hello"
print(hasattr(msg, "upper"))
Question 5 of 17 • medium
What does callable(obj) check?
Question 6 of 17 • medium
What does this print?
class Box:
    size = 10
b = Box()
print(getattr(b, "size", 0))
Question 7 of 17 • medium
Which line has a bug if you want to safely get attribute 'color' from obj with a fallback of 'red'?
Question 8 of 17 • medium
What does this print?
def greet(): pass
print(callable(greet))
Question 9 of 17 • hard
Which built-in displays the interactive documentation and help text for an object?
Question 10 of 17 • hard
What does this print?
class Point: pass
p = Point()
setattr(p, "x", 5)
print(getattr(p, "x"))
Question 11 of 17 • medium
Why is `isinstance(x, MyClass)` generally preferred over `type(x) == MyClass`?
Question 12 of 17 • easy
Where are function parameter and return type hints stored in Python at runtime?
Question 13 of 17 • medium
What does `inspect.signature(func)` return?
Question 14 of 17 • medium
What happens if you call `getattr(obj, 'absent_field')` without specifying a default value?
Question 15 of 17 • hard
Why does `callable(MyClass)` evaluate to True when `MyClass` is a class definition?
Question 16 of 17 • hard
What does `func.__qualname__` provide that `func.__name__` does not?
Question 17 of 17 • easy
What built-in function returns the unique integer identity (memory address in CPython) of an object?

15 / Guided Practice Labs & Test Suites

Lab 1

Runtime Parameter Type Validator

Difficulty: easy

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.

Starter Code
def validate_arguments(func, **kwargs):
    # Check kwargs against func.__annotations__
    pass
Automated Verification Assertions
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
Lab 2

Filter Public Callable Methods on Any Object

Difficulty: medium

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 '_').

Starter Code
def list_public_methods(obj):
    # Return sorted list of callable public method names
    pass
Automated Verification Assertions
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']
Lab 3

Batch Attribute Extractor with Fallbacks

Difficulty: hard

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.

Starter Code
def pluck_attributes(obj, attribute_map):
    # Extract attributes using getattr with fallback defaults
    pass
Automated Verification Assertions
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

Issue: Subclass Verification Failure Using type() Equality
Symptom: A payment gateway rejected CustomBillingRequest instances even though it inherited from BillingRequest.
Root Cause: The validator check was written as if type(req) is BillingRequest:, which failed because type(req) was CustomBillingRequest.
Resolution: Replace with if isinstance(req, BillingRequest): to accommodate subclasses properly.
Issue: Unhandled AttributeError in Configuration Parsing
Symptom: A worker crashed with AttributeError: 'Server' object has no attribute 'max_connections' during startup.
Root Cause: The code called getattr(server, 'max_connections') without providing a fallback default.
Resolution: Pass a fallback: getattr(server, 'max_connections', 100).

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.
Module 05 Milestone Achieved

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.

Advance to Module 06

Worked example

Inspect a function's name and documentation at runtime.

Example
def ping(): """Send ping packet.""" pass
print(f"{ping.__name__}: {ping.__doc__.strip()}")

How it works, step by step

  1. Functions store their identifier in __name__ and docstring in __doc__.

Your notes

Sign in to keep private notes alongside this lesson.

Sign in to take notes

Code Introspection & Metadata practice

Sign-in required

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

Locked

You must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.

Lesson incomplete

Not completed

Create an ID or sign in to save lesson completion across devices.

Create ID to save
Module lesson progress0/6 complete
Module cheat sheet