I think the documentation is pretty clear: there is no guarantee of
exactly what dir(obj) will return, whether it will return all methods
and attributes of an object, or only some, or even potentially add
entries that don’t correspond to any actual attribute.
(That would be weird, but not forbidden by the docs. I don’t know why
you would do that, but you could.)
Unless a class documents that dir() will support “everything you need
and nothing you don’t”, then you really shouldn’t be using it for
anything more than a convenience for interactive use.
(And not really that convenient, in my humble opinion. Too many dunders,
too many private single-underscore names, I nearly never want to see
them. But YMMV.)
Right now, if dir() returns an incomplete list of attributes, that’s
“not a bug”, just an inconvenience that the class writer may or may not
care about fixing.
Unfortunately there is no bulletproof function or method for iterating
over the attributes of an object. Perhaps we should reconsider the
“convenience” warning and document that dir is expected to give a
complete set of attributes, not just a convenient set. Then we can
fairly say that any object where dir() is incomplete is a bug in the
object.
You give this example:
properties = [(p, getattr(obj, p, "None")) for p in dir(obj) if
not (p[:2] == '__' or inspect.ismethod(getattr(obj, p)) or
inspect.isbuiltin(getattr(obj,p)))]
What’s the intent here? My guess it is to skip dunders and
callables, and yield … what? All other attributes? Only
properties?
Is it all callables that should be skipped or just those that are
builtin functions and methods? Should it list class methods, static
methods, other descriptors, callable instances, function objects?
If there is an attribute called ‘__’ should it be included? Currently it
isn’t. How about an instance attribute ‘__x’. Again, currently that’s
skipped, but being on the instance, it won’t be name-mangled.
Introspection is hard.
I prefer something like this:
properties = [(name, obj) for name, obj in vars(obj)
if not (isdunder(name) or callable(obj))]
where “isdunder” is a utility function:
def isdunder(string):
return (string.startswith('__')
and string.endswith('__')
and string != '__')
but my version won’t be quite identical to the existing version. For
starters, mine only looks at attributes on the instance, not class
attributes, or slots, or superclasses, or dynamic attributes.
Did I mention introspection is hard?
And of course I may have completely misunderstood the intent of the
code.