Python Module 18 – Advanced Python: data model, metaprogramming and internals
Introspection: inspect, vars() and type()
Inspect objects at run time with type(), vars(), dir() and the inspect module: read signatures and source code, and list the functions a module defines.
What you will learn
- Inspect objects, classes and modules at run time with type(), vars(), dir() and getattr()
- Read a function's parameters, kinds, defaults and annotations with inspect.signature
- Retrieve source code with inspect.getsource, and handle the errors it raises
- Decide when introspection helps and when explicit registration is clearer
Before you start
What you’ll learn · Before you start
What you will learn
- Inspect objects, classes and modules at run time with type(), vars(), dir() and getattr()
- Read a function's parameters, kinds, defaults and annotations with inspect.signature
- Retrieve source code with inspect.getsource, and handle the errors it raises
- Decide when introspection helps and when explicit registration is clearer
Before you start
On this page
A command-line tool can print the help for deploy(), every option with its kind and its default, without anyone
typing the options by hand: it asks the function for its parameters while the program runs. That is
introspection, a program examining its own objects: what type a value has, which attributes it holds, which
parameters a function takes and where its source code lives.
Python answers the everyday questions with built-in functions: type() and isinstance() for the class, vars() for
an object’s own attributes, dir() for every name it answers to, getattr() and hasattr() for one attribute at a
time, and callable() for whether something can be called. The inspect module goes further: inspect.signature()
describes how a function is called, inspect.getmembers() lists what a module or class contains, and
inspect.getsource() returns source text.
type(), vars() and dir() answer different questions
everyday_tools.py asks one ticket, its class and a module the everyday questions:
import json
class Ticket:
fare = 450
def __init__(self, passenger, seat):
self.passenger = passenger
self.seat = seat
def label(self):
return f"{self.passenger} {self.seat}"
t = Ticket("Asha", "B2-31")
print(type(t).__name__, type(t) is Ticket, isinstance(t, object), type(Ticket).__name__)
print(vars(t)) # the instance's own attributes
print([n for n in vars(Ticket) if not n.startswith("__")]) # what the class body defined
print([n for n in dir(t) if not n.startswith("__")]) # all of it, sorted, inherited names too
print(getattr(t, "seat"), getattr(t, "coach", "no coach"), hasattr(t, "label"), callable(t.label))
print(type(json).__name__, "dumps" in vars(json), len(dir(t)) > len(vars(t)) + len(vars(Ticket)))
class Point:
__slots__ = ("x", "y")
try:
vars(Point())
except TypeError as e:
print("TypeError:", e) Output
Ticket True True type
{'passenger': 'Asha', 'seat': 'B2-31'}
['fare', 'label']
['fare', 'label', 'passenger', 'seat']
B2-31 no coach True True
module True True
TypeError: vars() argument must have __dict__ attribute
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 everyday_tools.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
type(t)is the class itself, and the class’s own type istype.isinstance()is the better test in program logic, because it accepts subclasses too.vars(t)is the instance’s own attribute dictionary, andvars(Ticket)holds what the class body defined.dir()merges the names of the instance, its class and every base into one sorted list, which is why it is longer than both together: the extra names come fromobject.getattr()with a default answers “this attribute, or that value”, andhasattr()andcallable()answer yes or no. A module is an object like any other:type(json)ismodule, andvars(json)is its namespace.- An object without a
__dict__, such as an instance of a class with__slots__, has novars().
For the ticket t, the names come from three places, and vars() shows one of them at a time:
What dir() and vars() show for an instance t of the class Ticket
Text description of the diagram
The diagram is one large box labelled "dir(t): every name t answers to, sorted", holding three smaller boxes in a column.
- The first is vars(t), the instance's own attributes: passenger and seat.
- The second is vars(Ticket), what the class body defined: fare, label and __init__.
- The third holds the names that come from the bases, here only object: __repr__, __eq__, __class__ and many more.
Arrows marked "then" lead from each small box to the next, in the order attribute lookup searches them. dir() merges the names of all three into one sorted list, while vars() returns a single one of the dictionaries, with values.
The table sums up what each tool answers, and the catch to keep in mind with each:
| Tool | What it answers, and what to keep in mind |
|---|---|
type(obj), isinstance(obj, C) |
What class it is; isinstance accepts subclasses, type(obj) is C does not |
vars(obj) |
What it stores itself; TypeError for objects without a __dict__ |
dir(obj) |
Which names it answers to; meant for exploring, and its exact contents may change between versions |
hasattr(), getattr() |
Whether it has one attribute; both read the attribute, so a property’s code runs |
callable(obj) |
Whether it can be called; classes are callable, and so are objects with __call__ |
Checkpoint
Check: type(), vars() and dir()
A signature lists every parameter with its kind and default
cli_help.py writes the help of two commands from nothing but their signatures and docstrings:
import inspect
def deploy(service: str, /, version: str, *, region: str = "ap-south-1", dry_run: bool = False) -> None:
"""Deploy a service to one region."""
def rollback(service: str, steps: int = 1) -> None:
"""Roll a service back by a number of releases."""
def help_for(func):
"""Usage text built from the function's signature and docstring."""
sig = inspect.signature(func)
lines = [f"{func.__name__}{sig}", f" {inspect.getdoc(func)}"]
for p in sig.parameters.values():
annotation = "" if p.annotation is p.empty else p.annotation.__name__
default = "required" if p.default is p.empty else f"default {p.default!r}"
lines.append(f" {p.name:8} {annotation:5} {p.kind.description:22} {default}")
return "\n".join(lines)
for command in (deploy, rollback):
print(help_for(command)) Output
deploy(service: str, /, version: str, *, region: str = 'ap-south-1', dry_run: bool = False) -> None
Deploy a service to one region.
service str positional-only required
version str positional or keyword required
region str keyword-only default 'ap-south-1'
dry_run bool keyword-only default False
rollback(service: str, steps: int = 1) -> None
Roll a service back by a number of releases.
service str positional or keyword required
steps int positional or keyword default 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cli_help.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
inspect.signature(func) returns the function’s signature: A function's parameters with their kinds and defaults, as inspect.signature() and help() show them, such as (text, /, minutes, *, repeat=False, sound='chime'). More in “Parameters: defaults, keyword-only, positional-only” as a Signature object, whose
parameters map each name to a Parameter with a kind, a default and an annotation; a missing default or
annotation is the marker Parameter.empty, never None, so that a real default of None stays visible. Printing the
signature reproduces the parameter list, / and * included. The kinds follow those markers: parameters before / are
positional-only parameter: A parameter before / in a signature, such as obj in len(obj, /). A call passes it by position only, never by its name. More in “Parameters: defaults, keyword-only, positional-only”, those after * are keyword-only parameter: A parameter after * or *args in a signature, such as key in sorted(iterable, /, *, key=None, reverse=False). A call must pass it by name. More in “Parameters: defaults, keyword-only, positional-only”, and
the rest can be passed either way. The help text needed no list of options typed by hand, so it cannot fall out of step
with the functions. signature() also accepts classes, bound methods, functools.partial objects and many built-ins,
and on Python 3.14 it can return annotations as the text written in the source, without evaluating them.
Checkpoint
Check: Reading a signature
getmembers lists a module’s contents, imports included
main.py lists the functions of billing.py, then narrows the list to the ones the module defines itself:
Python (main.py) · members/main.py
import inspect
import json
import billing
functions = inspect.getmembers(billing, inspect.isfunction) # (name, value) pairs, sorted by name
print([name for name, _ in functions])
public = [
name
for name, func in functions
if not name.startswith("_") and inspect.getmodule(func) is billing
]
print(public)
print([name for name, _ in inspect.getmembers(billing, inspect.isclass)])
print(inspect.ismodule(billing), inspect.ispackage(billing), inspect.ispackage(json))
print(inspect.getdoc(billing.total), "|", billing.invoice_text("Asha", 100)) Output
['_round_up', 'dedent', 'invoice_text', 'total'] ['invoice_text', 'total'] ['Invoice'] True False True The amount with tax. | Asha: 118.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py
Python (billing.py) · members/billing.py
"""Billing helpers."""
from math import ceil # a built-in function: not a Python function at all
from textwrap import dedent # a Python function, but textwrap's, not ours
TAX_RATE = 0.18
def total(amount):
"""The amount with tax."""
return round(amount * (1 + TAX_RATE), 2)
def invoice_text(name, amount):
"""A short invoice, as text."""
return dedent(f"""
{name}: {total(amount)}
""").strip()
def _round_up(amount): # private by convention
return ceil(amount)
class Invoice:
"""An invoice record.""" Output
It printed nothing.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 billing.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
inspect.getmembers(billing, inspect.isfunction) returns (name, value) pairs sorted by name, reading each attribute
to get its value. Step through the names it reads, in sorted order, and the three tests that narrow them to the public
functions:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Invoice. getmembers() reads every name that dir(billing) lists. Invoice is a class, so inspect.isfunction says no; inspect.isclass would have kept it.
- TAX_RATE and the dunders. TAX_RATE is a float, and the names with two underscores on each side, such as __name__ and __file__, are not functions either.
- _round_up. _round_up is a Python function defined in billing, so getmembers() keeps it, but the leading underscore marks it private, so the public list drops it.
- ceil. ceil is a built-in function, written in C. inspect.isfunction is true only for functions written in Python, so getmembers() leaves it out.
- dedent. dedent is a Python function, so getmembers() keeps it, although billing only imported it. inspect.getmodule() looks up where it was defined, textwrap, and the test fails.
- invoice_text. A Python function, defined in billing, with a public name, so it passes every test.
- total. total passes in the same way. The public list ends as invoice_text and total, which is what the second line of output shows.
The list includes dedent, a Python function that billing merely imported, and leaves out ceil, which is a
built-in function, not a Python one. inspect.getmodule() tells them apart by where each function was defined, and the
leading underscore excludes the private helper. The other predicates work the same way, isclass here, and
inspect.ispackage(), new in Python 3.14, says whether a module is a package.
Checkpoint
Check: Listing what a module defines
Source code comes from the file, so some objects have none
source_code.py writes a module at run time and reads a function’s source back, then asks for two that have none:
import importlib
import inspect
from pathlib import Path
# Write a small module next to this file, as an editor would, then import it.
Path("rules.py").write_text(
"def discount(total):\n"
' """Ten per cent off orders above 1,000."""\n'
" return total * 0.9 if total > 1000 else total\n"
)
importlib.invalidate_caches() # let the import system notice a file created after start-up
import rules
print(inspect.getsource(rules.discount), end="")
print(Path(inspect.getsourcefile(rules.discount)).name, inspect.getsourcelines(rules.discount)[1])
namespace = {}
exec("def made_at_run_time(x):\n return x * 2\n", namespace)
for obj in (len, namespace["made_at_run_time"]):
try:
inspect.getsource(obj)
except (TypeError, OSError) as e:
print(f"{type(e).__name__}: {e}") Output
def discount(total):
"""Ten per cent off orders above 1,000."""
return total * 0.9 if total > 1000 else total
rules.py 1
TypeError: module, class, method, function, traceback, frame, or code object was expected, got builtin_function_or_method
OSError: could not get source code
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 source_code.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
getsource() reads the file a function came from, using the code object’s file name and first line number, so it
works for any Python code whose source file still exists. The example writes rules.py at run time and calls
importlib.invalidate_caches() first, because the import system caches directory listings and may otherwise miss a
file created after start-up. Follow the three objects through what getsource() needs:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- rules.discount. discount is a Python function. Its code object records the file it came from, rules.py, and its first line, line 1.
- The file is read. getsource() reads rules.py and returns the block that starts at line 1, the three lines of discount, which the program printed.
- len. len is a built-in function, written in C. It has no Python code object, so getsource() stops at the first need with TypeError.
- Code made by exec(). made_at_run_time was compiled by exec() from a string, so its code object names no file that exists, and getsource() raises OSError.
A built-in function has no Python source, so getsource(len) raises TypeError, and code created with exec() has no
file, so asking for its source raises OSError.
A few more tools of the module handle the harder cases. inspect.unwrap() follows the __wrapped__ chain that
functools.wraps leaves on a decorated function, and signature() follows it too. Signature.bind() checks a call
against a signature before the call is made. inspect.getattr_static() reads an attribute without running a property or
__getattr__. And inspect.currentframe() and inspect.stack() show the chain of calls that led to the current line.
Checkpoint
Check: Where source code comes from
Practise: types, signatures, members and source
Practice asks about the four ideas above, weakest first, and tells you at once why each answer is right or not.
Practice that adapts to you
Practise this lesson
9 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Signatures in depth: decorators, checked calls and annotations read as text Pro: opens with a Pro, Premium or Ultimate pass
Tools that build on signatures break in three predictable places: decorators, bad calls and annotations that cannot be evaluated yet. This part reproduces each one, fixes it with the standard tools, and ends with an exercise.
In this part:
- What a decorator does to a signature, and how wraps restores it
- Signatures of classes, methods, partials and built-ins
- Checking a call before making it, with Signature.bind
- Annotations read as text, without running them
- Exercise: describe a function without evaluating its annotations
- Describe a function's parameters without evaluating its annotations
1 diagram · 4 runnable examples (Python) · 1 exercise with sample tests · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Introspection without side effects: properties that fire, getmembers counted, and when to stop Pro: opens with a Pro, Premium or Ultimate pass
Reading an attribute can run code, so careless introspection opens connections, computes reports or raises in the
middle of a health check. This part counts what hasattr() and getmembers() really execute, shows the static
alternatives, and weighs introspection against explicit registration.
In this part:
- hasattr() runs code: a property firing, reproduced
- What getmembers() reads, counted, and what its static version skips
- Attributes that getmembers() cannot see
- Who called this function: frames, and the reference cycle they create
- Introspection or explicit registration: choosing
1 diagram · 4 runnable examples (Python) · about 10 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What is the difference between dir() and vars()?
vars(obj) returns the object's own attribute dictionary, its __dict__, with the values: for an instance, the attributes set on it; for a class, what its body defined; for a module, its namespace. It raises TypeError for objects without a __dict__, such as instances of classes with __slots__. dir(obj) returns a sorted list of names only, gathered from the object, its class and all the bases, so it includes inherited methods and the names that come from object; a class can change what it reports with __dir__. Use vars() to see or copy an object's own state, and dir() to explore what an object offers, at the interactive prompt or in tab completion. The documentation describes dir() as a convenience whose exact contents may change between versions, so program logic should not depend on it.
-
Mid to senior
Your web framework builds request validators from handler signatures with inspect.signature(). After someone adds a timing decorator to every handler, all handlers accept any arguments and validation silently stops. Diagnose the bug, and make the framework robust against it.
-
Mid to senior
A health-check endpoint calls hasattr(client, 'session') on 40 client objects. After a library upgrade its response time jumps from 5 ms to 2 s, and sometimes it raises ConnectionError. Explain what is happening and how you would check for the attribute without side effects.
-
Mid to senior
A rules feature shows users the source of their rule functions with inspect.getsource(). It works on developers' machines but raises OSError in the deployed app. Give the likely causes and the alternatives you would consider.
Key takeaways
type()andisinstance()answer what class an object has;vars()gives its own attributes;dir()lists every name it answers to.inspect.signature()describes parameters by name, kind, default and annotation;Parameter.emptymarks what is missing.inspect.getmembers()with a predicate lists a module’s or class’s members;inspect.getmodule()separates what a module defines from what it imports.inspect.getsource()needs a Python source file: built-ins raiseTypeErrorand code without a file raisesOSError.
Terms in this lesson
- introspection
- A program examining its own objects while it runs: their type, their attributes, a function's parameters or its source code, with built-ins such as type(), vars() and dir() and with the inspect module.
References
- inspect: inspect live objects (Python Software Foundation)
- inspect: introspecting callables with the Signature object (Python Software Foundation)
- inspect: retrieving source code (Python Software Foundation)
- inspect: fetching attributes statically (Python Software Foundation)
- Built-in Functions: vars() (Python Software Foundation)
- Built-in Functions: dir() (Python Software Foundation)
- annotationlib: functionality for introspecting annotations (Python Software Foundation)
- importlib.invalidate_caches() (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress