Inspect.getmembers() seems to show extra members on Python >= 3.7

Hi all,

I’ve encountered a weird issue with inspect.getmembers() and would like some help.

Background:
I’ve been adding type hints to a project. One of my classes is a generic container, so I made a type variable _T = TypeVar('_T') so I could write class MyContainer(Deque[_T]) and not export the variable from the module.

Problem:
There is a test that uses importlib and inspect to make sure only the public API is exported.
For some reason in Python >= 3.7, inspect.getmembers() shows _T as one of the exports despite the leading underscore and despite __all__ excluding _T. This is causing the test fail.

A simplified example and results can be seen here:

What changed between 3.6 and 3.7? Am I using inspect.getmembers() incorrectly?

Thanks.

1 Like

Digging into this further, it seems that the change was not in inspect.getmembers(), but either inspect.getmodule() or typing.TypeVar.

This is basically the test I was using:

# return a set of strings that represent the names of objects defined in that module
import inspect
import importlib

module = importlib.import('mod')

return {n for n, o in inspect.getmembers(module, lambda m: inspect.getmodule(m) is module)}

The lambda there is supposed to filter out things not defined in the module. Using the code from my GitHub link above, I get these interesting results:

Python 3.6:

>>> import importlib
>>> import inspect
>>> module = importlib.import_module('mod')
>>> inspect.getmodule(module._D)
<module 'typing' from '/Users/jmorris/.pyenv/versions/3.6.12/lib/python3.6/typing.py'>

Python 3.7:

>>> import importlib
>>> import inspect
>>> module = importlib.import_module('mod')
>>> inspect.getmodule(module._D)
<module 'mod' from '/Users/jmorris/src/ci-experiments/mod.py'>

Ok, yeah typing.TypeVar was changed in Python 3.7. There were apparently issues with pickling generic types and part of the fix was setting the module that type variables are defined in.

I think I’m going to have to rethink my test though, because it still fails with code like this:

mod.py:

__all__ = ['A', 'B']

class A:
    pass

class B:
    pass

class _C:
    pass