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), and `Cls.__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)` while `Leaf.__mro__` is `(Leaf, Mid, Base, object)`.
- `object.__bases__` is the empty tuple `()` because `object` is the root of the hierarchy; every other new-style class has at least one base.
- Instances do **not** inherit `__name__`: `instance.__name__` raises `AttributeError`, so class names must be read via `type(obj).__name__` or `obj.__class__.__name__`.
- `hasattr(obj, 'x')` searches the instance namespace and the whole class hierarchy and takes the attribute name as a **string**; `is` and `is not` compare object identity (the `id()` 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:
What is printed?class Base: pass
class Mid(Base): pass
class Leaf(Mid, Base): pass
print(len(Leaf.__bases__), len(Base.__bases__), len(object.__bases__))
What happens when the following code is executed?
class Sensor:
def __init__(self, tag):
self.tag = tag
s = Sensor("T-9")
print(s.__name__)
Given class Widget: pass defined in a directly executed script, which statement about class introspection is TRUE?
Consider the following code:
What is printed?class Node: pass
a = Node()
b = a
c = Node()
print(a is b, a is not c, type(a) is Node)