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.
Last updated: August 2026

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:

  1. object.__bases__ is the empty tuple. object is the root of the hierarchy and has no parent.
  2. __bases__ is not the MRO. It stops at the first level. The full linearization comes from __mro__ (a tuple) or mro() (a list):
print(Leaf.__bases__)                       # (Mid, Base)          -> depth 1 only
print([c.__name__ for c in Leaf.__mro__])   # ['Leaf', 'Mid', 'Base', 'object']
PropertyTypeScopeAvailable on an instance?
__name__strThe class onlyNo — AttributeError
__module__strDefining moduleYes (inherited)
__bases__tupleDirect parents onlyNo — AttributeError
__mro__tupleFull linearizationNo — AttributeError
__dict__mappingproxy on a class, dict on an instanceNamespace contentsYes (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 is only for identity checks and singletons such as None, True, and False. Comparing values with is (for example x is 1000) is unreliable because it depends on interpreter caching, not on the data.

Loading diagram...
Class Introspection Attributes and Where They Resolve
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

What happens when the following code is executed?

class Sensor:
    def __init__(self, tag):
        self.tag = tag

s = Sensor("T-9")
print(s.__name__)

A
B
C
D
Test Your Knowledge

Given class Widget: pass defined in a directly executed script, which statement about class introspection is TRUE?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D