Introspection with inspect

Reviewed & published by Brayan K

The inspect module lets your program examine its own functions, classes, and modules at runtime — reading signatures, source code, and members on the fly.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

It's the engine behind documentation generators, test frameworks, and CLI builders. Learn it and you can write tools that adapt to any code handed to them.

What You'll Learn in This Lesson

1 inspect.signature: Read a Function's Parameters

inspect.signature(func) returns a Signature object whose .parameters maps each parameter name to details — its kind (positional, keyword-only, etc.) and its default value. This is how a CLI tool figures out what arguments a command takes:

import inspect

def greet(name, greeting="Hello", *, loud=False):
    return f"{greeting}, {name}!"

sig = inspect.signature(greet)
print("signature:", sig)
# signature: (name, greeting='Hello', *, loud=False)

for pname, param in sig.parameters.items():
    has_default = param.default is not inspect.Parameter.empty
    print(pname, "| kind:", param.kind.name, "| has default:", has_default)
import inspect

def make_user(username, age=18, *, admin=False):
    return (username, age, admin)

sig = inspect.signature(make_user)
print("repr:", sig)

required = [
    name for name, p in sig.parameters.items()
    if p.default is inspect.Parameter.empty and p.kind.name != "VAR_KEYWORD"
]
print("required params:", required)

# ✅ Expected output:
# repr: (username, age=18, *, admin=False)
# required params: ['username']

2 getsource & Type Tests

inspect.getsource(obj) returns the exact source text of a function or class — as long as it was defined in a real file. The is* predicates (isfunction, isclass, ismethod, ismodule) tell you what kind of object you're holding:

import inspect

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

# getsource works for objects defined in a .py file
src = inspect.getsource(add)
print(src, end="")
# def add(a, b):
#     return a + b

print("lines:", len(src.splitlines()))   # lines: 2

# Type predicates
print("isfunction(add):", inspect.isfunction(add))   # True
print("isclass(add):", inspect.isclass(add))         # False
import inspect

def square(n):
    return n * n

class Box:
    pass

print("square is a function:", inspect.isfunction(square))
print("Box is a class:", inspect.isclass(Box))
print("square is a class:", inspect.isclass(square))

source = inspect.getsource(square)
print("source line count:", len(source.splitlines()))

# ✅ Expected output:
# square is a function: True
# Box is a class: True
# square is a class: False
# source line count: 2

3 getmembers & getfullargspec

getmembers(obj, predicate) returns every member that passes a test — pass inspect.isfunction to list a class's methods. getfullargspec(func) breaks a signature into plain lists, handy when you want simple names for positional, keyword-only, and variadic args:

import inspect

class Robot:
    wheels = 4
    def move(self): return "moving"
    def stop(self): return "stopped"

# List just the methods (members that are functions)
methods = [name for name, obj in inspect.getmembers(Robot, inspect.isfunction)]
print("methods:", methods)   # methods: ['move', 'stop']

def configure(host, port=8080, *args, debug=False, **kwargs):
    pass

spec = inspect.getfullargspec(configure)
print("args:", spec.args)             # ['host', 'port']
print("varargs:", spec.varargs)       # args
print("kwonlyargs:", spec.kwonlyargs) # ['debug']
print("varkw:", spec.varkw)           # kwargs

Together these let you walk an unknown object and discover its shape — which methods it offers and how to call them — the heart of "auto-wiring" frameworks.

import inspect

class Service:
    def start(self): ...
    def stop(self): ...
    def status(self): ...

# Auto-discover the public method names
api = [name for name, _ in inspect.getmembers(Service, inspect.isfunction)]
print("api:", api)
print("method count:", len(api))

def handler(request, retries=3, *, timeout=30):
    ...

spec = inspect.getfullargspec(handler)
print("positional:", spec.args)
print("keyword-only:", spec.kwonlyargs)

# ✅ Expected output:
# api: ['start', 'status', 'stop']
# method count: 3
# positional: ['request', 'retries']
# keyword-only: ['timeout']

🎯 YOUR TURN: Fill in the Blanks

Fill in each ___ so the code reports the function's parameter names. (Hint: the modern API is inspect.signature, and its parameters live in .parameters.)

import inspect

def send(to, subject, body="", cc=None):
    ...

# Get the Signature object -> use inspect.___
sig = inspect.___(send)

# .parameters is a mapping of name -> Parameter
names = list(sig.___.keys())
print("params:", names)
print("count:", len(names))

# Expected output:
# params: ['to', 'subject', 'body', 'cc']
# count: 4

Common Errors

❌ Using == to check for "no default"

# Comparing to a sentinel with == can misbehave; use 'is'
if param.default == inspect.Parameter.empty:   # fragile

✅ Compare the sentinel with is

import inspect
def f(a, b=1): ...
p = inspect.signature(f).parameters["a"]
print(p.default is inspect.Parameter.empty)   # True

❌ Calling getsource on REPL / built-in objects

import inspect
inspect.getsource(len)
# TypeError: module, class, method, function, ... expected (no Python source)

✅ Use it on your own file-defined functions

import inspect
def my_func(): return 1
print(inspect.getsource(my_func).splitlines()[0])   # def my_func(): return 1

❌ Expecting getmembers to skip dunder methods

# getmembers also returns inherited and __dunder__ members

✅ Filter out the names you don't want

import inspect
class C:
    def go(self): ...
public = [n for n, _ in inspect.getmembers(C, inspect.isfunction)
          if not n.startswith("_")]
print(public)   # ['go']

🎯 Mini Challenge: Auto-Generate Help

Build a tiny help generator: for every function in a small registry, print its name and signature using inspect.signature — exactly how CLI frameworks build their --help output.

import inspect

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

def scale(x, factor=2):
    return x * factor

def clamp(value, low=0, high=100):
    return max(low, min(value, high))

commands = {"add": add, "scale": scale, "clamp": clamp}

print("Available commands:")
for name in sorted(commands):
    sig = inspect.signature(commands[name])
    print(f"  {name}{sig}")

# ✅ Expected output:
# Available commands:
#   add(a, b)
#   clamp(value, low=0, high=100)
#   scale(x, factor=2)

📚 Keep Learning

Frequently Asked Questions

Lesson complete — your code can read itself!

You can read parameters with inspect.signature, fetch source with getsource, classify objects with isfunction and isclass, list members with getmembers, and dissect arguments with getfullargspec — the toolkit behind real framework magic.

Practice quiz

What does the inspect module let your program do?

  • Speed up function calls
  • Compile Python to C
  • Examine its own functions, classes, and modules at runtime
  • Encrypt source code

Answer: Examine its own functions, classes, and modules at runtime. inspect lets code introspect itself — reading signatures, source, and members at runtime.

What does inspect.signature(func) return?

  • A Signature object with a .parameters mapping
  • The function's return value
  • The function's source code
  • A list of argument names only

Answer: A Signature object with a .parameters mapping. signature returns a Signature object whose .parameters maps each name to a Parameter with kind and default.

How do you tell a required parameter from an optional one?

  • Check if its name starts with an underscore
  • It appears first in the list
  • Its kind is VAR_KEYWORD
  • Its default is inspect.Parameter.empty

Answer: Its default is inspect.Parameter.empty. A required parameter has no default, represented by the sentinel inspect.Parameter.empty.

Why compare param.default against inspect.Parameter.empty with 'is' rather than '=='?

  • '==' is slower
  • It is a sentinel object, so identity comparison with 'is' is correct
  • 'is' works on strings only
  • There is no difference

Answer: It is a sentinel object, so identity comparison with 'is' is correct. Parameter.empty is a sentinel; compare against it with 'is' (identity), not '=='.

What does inspect.getsource(obj) return?

  • The exact source text of the object, if defined in a real file
  • The bytecode
  • The docstring only
  • The function's signature

Answer: The exact source text of the object, if defined in a real file. getsource returns the source text, as long as the object was defined in a real .py file.

Why does inspect.getsource sometimes raise OSError?

  • The function is too large
  • The function has no arguments
  • There is no source file (e.g. typed at the REPL or built with exec)
  • The object is a string

Answer: There is no source file (e.g. typed at the REPL or built with exec). getsource needs a real source file; objects from the REPL or exec have none, raising OSError/TypeError.

For a normal def add(a, b), what does inspect.isfunction(add) return?

  • False
  • True
  • None
  • An error

Answer: True. isfunction is True for a Python function defined with def; isclass(add) would be False.

What does inspect.getmembers(Robot, inspect.isfunction) give you?

  • Every attribute of Robot
  • Only data attributes
  • The source of Robot
  • Only the members that are functions (its methods)

Answer: Only the members that are functions (its methods). getmembers(obj, predicate) returns members passing the test — isfunction lists the class's methods.

For def configure(host, port=8080, *args, debug=False, **kwargs), what is getfullargspec(...).kwonlyargs?

  • ['host', 'port']
  • ['debug']
  • ['args']
  • ['kwargs']

Answer: ['debug']. Keyword-only parameters appear after the *args; here only debug is keyword-only.

What does getmembers return that you often need to filter out?

  • Only the methods you defined
  • Nothing extra
  • Inherited and __dunder__ members too
  • Only data attributes

Answer: Inherited and __dunder__ members too. getmembers also returns inherited and dunder members, so you usually filter names starting with _.

Continue this course

Frequently asked questions

What is the inspect module used for?

inspect lets your program examine itself at runtime — it can read a function's signature and parameters, fetch its source code, list the members of a class or module, and tell whether an object is a function, class, method, or module. It powers tools like documentation generators, test frameworks, dependency injectors, and CLI builders.

What's the difference between inspect.signature and getfullargspec?

Both describe a function's parameters. inspect.signature is the modern, richer API: it returns a Signature object with a clean repr and a parameters mapping that includes kinds, defaults, and annotations, and it works with decorators and partials. getfullargspec returns a plain named tuple of lists and is handy when you want simple lists of arg names. Prefer signature for new code.

Why does inspect.getsource sometimes fail?

getsource reads the source from the file the object was defined in. If the function was typed at an interactive prompt, built dynamically with exec, or compiled without source available, there's no file to read and it raises OSError (or TypeError). It works reliably for functions and classes defined in a real .py file, which is why the examples here define them in a script.

How do isfunction, isclass, and getmembers help build tooling?

They let you discover what's inside an object without knowing it in advance. getmembers(obj, predicate) returns every member that passes a test like inspect.isfunction, so you can auto-register all functions in a module, list all methods on a class, or build a plugin loader — the foundation of a lot of framework 'magic'.