5.4 Discovering Class Structure: __name__, __module__, __bases__, and Identity
Key Takeaways
Cls.__name__is the class's declared name as a string,Cls.__module__is the name of the module where the class was defined ('__main__'for a directly executed script,'builtins'for built-in types), andCls.__bases__is a tuple of direct superclasses.__bases__lists only immediate parents, while__mro__lists the entire linearized lookup chain —Leaf.__bases__may be(Mid, Base)whileLeaf.__mro__is(Leaf, Mid, Base, object).object.__bases__is the empty tuple()becauseobjectis the root of the hierarchy; every other new-style class has at least one base.Instances do not inherit
__name__:instance.__name__raisesAttributeError, so class names must be read viatype(obj).__name__orobj.__class__.__name__.hasattr(obj, 'x')searches the instance namespace and the whole class hierarchy and takes the attribute name as a string;isandis notcompare object identity (theid()of two references), never attribute values.
Discovering Class Structure: name, module, bases, and Identity
Because a class statement creates an object rather than a static template, Python can be asked at runtime what a class is called, where it was defined, and what it inherits from. Objective 4.4 names exactly three properties for this — __name__, __module__, and __bases__ — alongside the hasattr() function. This section covers all four, plus the is and is not identity operators the exam pairs with type introspection.
1. Classes Are Objects, So Classes Have Attributes
In Python a class statement does not merely describe a template — it creates an object and binds it to a name. Because the class itself is an object, it carries its own attributes, and the interpreter populates three of them automatically. Objective PCAP-31-03 4.4 names exactly these three plus the hasattr() function, and exam items routinely ask you to distinguish what lives on the class from what lives on the instance.
class Base:
pass
class Mid(Base):
pass
class Leaf(Mid, Base):
pass
2. __name__: The Class's Declared Name
Cls.__name__ returns the identifier used in the class statement, as a plain string:
print(Leaf.__name__) # 'Leaf'
print(Mid.__name__) # 'Mid'
print(int.__name__) # 'int'
Instances do not have __name__. This is the trap the exam sets:
obj = Leaf()
print(obj.__name__)
# AttributeError: 'Leaf' object has no attribute '__name__'
To recover a class name from an instance, go through the class first:
print(type(obj).__name__) # 'Leaf'
print(obj.__class__.__name__) # 'Leaf'
The same idiom appears in exception handling, where type(e).__name__ prints the exception class name without the module prefix.
3. __module__: Where the Class Was Defined
Cls.__module__ holds the name of the module in which the class statement executed:
print(Leaf.__module__) # '__main__' (defined in the directly executed script)
print(int.__module__) # 'builtins'
print(str.__module__) # 'builtins'
If Leaf had been defined in shapes.py and imported, Leaf.__module__ would be 'shapes'. Unlike __name__, __module__ is reachable from an instance, because attribute lookup falls through to the class:
print(obj.__module__) # '__main__'
That asymmetry — obj.__module__ works but obj.__name__ does not — is deliberate exam material. __name__ fails on instances because the metaclass type defines it as a descriptor on classes only, whereas __module__ is stored as an ordinary entry in the class dictionary and is therefore inherited by instances.
4. __bases__: The Tuple of Direct Superclasses
Cls.__bases__ is a tuple containing only the immediate parents listed in the class header, in written order:
print(Leaf.__bases__) # (<class '__main__.Mid'>, <class '__main__.Base'>)
print(Mid.__bases__) # (<class '__main__.Base'>,)
print(Base.__bases__) # (<class 'object'>,)
print(object.__bases__) # ()
print(bool.__bases__) # (<class 'int'>,)
Two facts are heavily tested:
object.__bases__is the empty tuple.objectis the root of the hierarchy and has no parent.__bases__is not the MRO. It stops at the first level. The full linearization comes from__mro__(a tuple) ormro()(a list):
print(Leaf.__bases__) # (Mid, Base) -> depth 1 only
print([c.__name__ for c in Leaf.__mro__]) # ['Leaf', 'Mid', 'Base', 'object']
| Property | Type | Scope | Available on an instance? |
|---|---|---|---|
__name__ | str | The class only | No — AttributeError |
__module__ | str | Defining module | Yes (inherited) |
__bases__ | tuple | Direct parents only | No — AttributeError |
__mro__ | tuple | Full linearization | No — AttributeError |
__dict__ | mappingproxy on a class, dict on an instance | Namespace contents | Yes (its own) |
A recursive base walk is a common scenario item:
def show_ancestry(cls, depth=0):
print(" " * depth + cls.__name__)
for parent in cls.__bases__:
show_ancestry(parent, depth + 1)
show_ancestry(Leaf)
# Leaf
# Mid
# Base
# object
# Base
# object
Notice that Base and object are visited twice: __bases__ describes the raw inheritance graph, whereas C3 linearization deduplicates it into a single ordered MRO.
5. hasattr(), getattr(), and Dynamic Introspection
hasattr(object, name) returns True when name resolves on the object or anywhere in its class hierarchy. The name must be passed as a string:
print(hasattr(obj, "__module__")) # True (inherited from Leaf)
print(hasattr(obj, "__name__")) # False (classes only)
print(hasattr(Leaf, "__name__")) # True
print(hasattr(Leaf, "__bases__")) # True
getattr(object, name[, default]) retrieves the value and, given a third argument, returns that default instead of raising:
print(getattr(obj, "missing", "fallback")) # 'fallback'
dir(Cls) lists every name reachable on the class, including inherited dunders — useful for exploration but too noisy to be a reliable exam answer.
Cls.__dict__ Is a mappingproxy, Not a dict
print(type(Leaf.__dict__).__name__) # 'mappingproxy'
print(type(obj.__dict__).__name__) # 'dict'
Leaf.__dict__["z"] = 1
# TypeError: 'mappingproxy' object does not support item assignment
A class namespace is read-only through __dict__; changes must go through attribute assignment (Leaf.z = 1). An instance __dict__ is an ordinary mutable dictionary.
6. Identity: is and is not
Objective PCAP-31-03 4.5 names the is and not is operators alongside isinstance(). is compares identity — whether two references point to the same object in memory — and is exactly equivalent to comparing id() values:
a = Leaf()
b = a
c = Leaf()
print(a is b) # True (same object)
print(a is c) # False (two distinct objects)
print(a is not c) # True
print(id(a) == id(b)) # True
The negated form is written is not, a single two-word operator; not is is not valid syntax on its own and the syllabus wording is shorthand for the negation.
is is also the correct tool for exact type pinning, which behaves differently from isinstance():
print(isinstance(a, Base)) # True — Leaf inherits from Base
print(type(a) is Leaf) # True — exact class match
print(type(a) is Base) # False — subclasses do not satisfy 'is'
Exam Trap: Use
isonly for identity checks and singletons such asNone,True, andFalse. Comparing values withis(for examplex is 1000) is unreliable because it depends on interpreter caching, not on the data.
Consider the following code:
class Base: pass
class Mid(Base): pass
class Leaf(Mid, Base): pass
print(len(Leaf.__bases__), len(Base.__bases__), len(object.__bases__))
What is printed?
2 1 0
4 2 1
2 1 1
1 1 0
What happens when the following code is executed?
class Sensor:
def __init__(self, tag):
self.tag = tag
s = Sensor("T-9")
print(s.__name__)
It prints 'Sensor' because __name__ is inherited from the class.
It prints 'T-9' because __name__ defaults to the first constructor argument.
An AttributeError is raised because __name__ is defined on classes, not on instances.
It prints '__main__' because __name__ reports the defining module.
Given class Widget: pass defined in a directly executed script, which statement about class introspection is TRUE?
Widget.__module__ evaluates to 'Widget' and Widget.__name__ evaluates to '__main__'.
Widget.__dict__ is an ordinary mutable dict, so Widget.__dict__['x'] = 1 adds a class attribute.
hasattr(Widget, 'name') accepts the attribute as a bare identifier rather than as a string.
Widget.__name__ evaluates to 'Widget' and Widget.__module__ evaluates to '__main__'.
Consider the following code:
class Node: pass
a = Node()
b = a
c = Node()
print(a is b, a is not c, type(a) is Node)
What is printed?
True False True
True True True
False True True
True True False
Sections you finish are checked off in the contents.