2025, Dec 05 07:00

Why inspect.getsource fails for dynamically imported Python classes and how sys.modules fixes it

Learn why inspect.getsource() and inspect.getsourcefile() fail for dynamically imported Python classes, and how registering modules in sys.modules fixes it.

Dynamic imports are a convenient way to load Python modules by path, but they sometimes collide with introspection tools. A common surprise appears when inspect.getsource() and inspect.getsourcefile() return source information for functions yet fail for classes, raising a misleading TypeError about a “built-in class.” This happens even though the class is clearly defined in your dynamically imported file.

Reproducing the issue

Consider a minimal two-file setup loaded with importlib. The function’s source is retrievable, the class’s source is not.

loader.py

import inspect
import os
import importlib.util

base_dir = os.path.dirname(__file__)
src_spec = importlib.util.spec_from_file_location("lib_dyn", os.path.join(base_dir, "lib_dyn.py"))
loaded_mod = importlib.util.module_from_spec(src_spec)
src_spec.loader.exec_module(loaded_mod)

print(loaded_mod.bump(3))
print(loaded_mod.Counter().plus(3))

print("module source file:", inspect.getsourcefile(loaded_mod))
for item in ["bump", "Counter"]:
    ref = getattr(loaded_mod, item)
    print(f"{item} source: {inspect.getsourcefile(ref)}")
    print(inspect.getsource(ref))

lib_dyn.py

def bump(n):
    return n + 1

class Counter(object):
    def plus(self, n):
        return n + 1

When run, the function’s source is printed, while attempting to retrieve the class source raises a TypeError stating it is a built-in class.

What’s actually going on

The behavior follows from how inspect resolves a class back to its defining module. The relevant logic for determining a source file for classes looks like this:

if isclass(object):
    if hasattr(object, '__module__'):
        module = sys.modules.get(object.__module__)
        if getattr(module, '__file__', None):
            return module.__file__
        if object.__module__ == '__main__':
            raise OSError('source code not available')
    raise TypeError('{!r} is a built-in class'.format(object))

Classes carry only the module name as a string in object.__module__. To turn that string into an actual module object, inspect looks it up in sys.modules. If the module isn’t present there, the branch above concludes that it must be a built-in class and raises the TypeError you’re seeing. In other words, the dynamic loading step didn’t register the module in sys.modules, and inspect has no other place to resolve it from. There isn’t an alternate inspect API here that lets you supply the module; inspect.getmodule(loaded_mod.Counter) is None in this situation, which underlines the reliance on sys.modules.

Am I missing something during my import step that tells Python how to get source info for classes?

Yes. The importlib documentation’s recipe for importing a source file by path shows an explicit sys.modules update, because exec_module doesn’t perform it for you.

The fix

Register the dynamically loaded module in sys.modules under the name you gave it in the spec. After that, inspect will be able to trace a class back to its defining file.

A small helper mirrors the importlib recipe:

import importlib.util
import sys

def load_from_path(mod_name, file_path):
    sp = importlib.util.spec_from_file_location(mod_name, file_path)
    mod = importlib.util.module_from_spec(sp)
    sys.modules[mod_name] = mod
    sp.loader.exec_module(mod)
    return mod

Applied to the earlier example:

loader_fixed.py

import inspect
import os
from importlib.util import spec_from_file_location, module_from_spec
import sys

root = os.path.dirname(__file__)
sp = spec_from_file_location("lib_dyn", os.path.join(root, "lib_dyn.py"))
modx = module_from_spec(sp)
sys.modules["lib_dyn"] = modx
sp.loader.exec_module(modx)

print(modx.bump(3))
print(modx.Counter().plus(3))

print("module source file:", inspect.getsourcefile(modx))
for name in ["bump", "Counter"]:
    obj = getattr(modx, name)
    print(f"{name} source: {inspect.getsourcefile(obj)}")
    print(inspect.getsource(obj))

This change is sufficient to make inspect return the class source without errors. If you are concerned about name collisions in sys.modules, you can choose a unique module name; the name is under your control when creating the spec.

Why this matters

Introspection features depend on the import system’s global registry. If a dynamically loaded module isn’t visible in sys.modules, tools like inspect cannot reliably map classes back to files. The resulting TypeError reads as if the class were built-in, which is confusing; even the OSError branch that reports “source code not available” would be less surprising. Ensuring that dynamically imported modules are registered makes function and class inspection consistent.

Takeaways

When loading modules by path with importlib, make sure to insert the module into sys.modules before executing it. This aligns with the documented approach and allows inspect.getsource() and inspect.getsourcefile() to work for both functions and classes. If you want to avoid clashes, pick a distinctive module name when creating the spec. Keep in mind that inspect has to recover the defining module from object.__module__, and sys.modules is where that lookup happens.