Skip to content

C3 Linearization

C3 linearization is the algorithm Python uses to compute the Method Resolution Order (MRO) for classes with multiple inheritance.


Algorithm Overview

1. Named After Paper

C3 linearization algorithm from Barrett et al.

2. Not Simple Traversal

Not depth-first or breadth-first—a specialized merge algorithm.

3. Three Key Properties

  • Preserves local precedence order
  • Maintains parent MRO consistency
  • Ensures monotonicity

The Formula

For a class C(B1, B2, ..., Bn):

\[ L[C] = [C] + \text{merge}(L[B1], L[B2], \ldots, L[Bn], [B1, B2, \ldots, Bn]) \]

where:

  • \(L[Bi]\) is the MRO of parent \(Bi\)
  • \([B1, B2, \ldots, Bn]\) is the list of direct parents

Merge Procedure

1. Look at Heads

Examine the first element of each list.

2. Find Valid Head

Pick the first head that is not in the tail of any other list.

3. Append and Remove

Append this head to result and remove it from all lists.

4. Repeat

Continue until all lists are empty.


Simple Example

1. Class Hierarchy

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

2. Compute Base MROs

  • \(L[A] = [A, \text{object}]\)
  • \(L[B] = [B, A, \text{object}]\)
  • \(L[C] = [C, A, \text{object}]\)

3. Apply Formula

\[ L[D] = [D] + \text{merge}([B, A, \text{object}], [C, A, \text{object}], [B, C]) \]

Step-by-Step Merge

1. Initial Lists

  • \([B, A, \text{object}]\) : head \(B\), tail \([A, \text{object}]\)
  • \([C, A, \text{object}]\) : head \(C\), tail \([A, \text{object}]\)
  • \([B, C]\) : head \(B\), tail \([C]\)

2. Pick B

\(B\) is a head and not in any tail → append \(B\).

Result: \([D, B]\)

Updated lists: - \([A, \text{object}]\) - \([C, A, \text{object}]\) - \([C]\)

3. Pick C

\(C\) is a head and not in any tail → append \(C\).

Result: \([D, B, C]\)

Updated lists: - \([A, \text{object}]\) - \([A, \text{object}]\) - \([]\)

4. Pick A

\(A\) is a head and not in any tail → append \(A\).

Result: \([D, B, C, A]\)

Updated lists: - \([\text{object}]\) - \([\text{object}]\) - \([]\)

5. Pick object

\(\text{object}\) is a head → append \(\text{object}\).

Result: \([D, B, C, A, \text{object}]\)

All lists empty—done!


Final MRO

\[ L[D] = [D, B, C, A, \text{object}] \]
print(D.mro())
# [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]

Complex Example

1. Multiple Paths

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
class E(D, C): pass

2. Compute Step-by-Step

  • \(L[D] = [D, B, C, A, \text{object}]\)
  • \(L[C] = [C, A, \text{object}]\)
\[ L[E] = [E] + \text{merge}([D, B, C, A, \text{object}], [C, A, \text{object}], [D, C]) \]

3. Merge Process

Following the same merge rules:

\(L[E] = [E, D, B, C, A, \text{object}]\)


Conflict Detection

1. When Merge Fails

If every head appears in some tail, merge fails.

2. TypeError Raised

class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass  # Error!

3. Error Message

TypeError: Cannot create a consistent method resolution order (MRO)

Why It Matters

1. Predictability

C3 ensures deterministic method resolution.

2. Cooperative Inheritance

Enables super() to work correctly.

3. Prevents Ambiguity

Guarantees consistency or rejects invalid hierarchies.


Practical Implications

1. Left Parents First

Parents listed first have higher priority.

class D(B, C):  # B's methods before C's
    pass

2. All Ancestors Once

Each class appears exactly once in MRO.

3. Monotonic Order

Parent order is preserved throughout the hierarchy.


Key Takeaways

  • C3 linearization computes MRO deterministically.
  • Uses merge algorithm on parent MROs.
  • Picks heads not in any tail.
  • Rejects inconsistent hierarchies.
  • Enables cooperative multiple inheritance.

Runnable Example: c3_linearization_examples.py

"""
06_c3_linearization.py

ADVANCED LEVEL: Understanding the C3 Linearization Algorithm

This file explains the C3 linearization algorithm that Python uses to compute
MRO. Understanding C3 helps you predict MRO for complex hierarchies and
understand why certain inheritance patterns fail.

Learning Objectives:
- Understand the C3 linearization algorithm
- Learn to compute MRO manually
- Understand consistency requirements
- Predict MRO for complex hierarchies
- Recognize invalid inheritance patterns
"""

# ============================================================================
# SECTION 1: What is C3 Linearization?
# ============================================================================

if __name__ == "__main__":

    """
    C3 Linearization (also called C3 Superclass Linearization) is the algorithm
    Python uses to determine Method Resolution Order.

    Created by: Michele Simionato and others
    Adopted in: Python 2.3 (2003)
    Purpose: Solve the diamond problem and provide consistent method resolution

    KEY PROPERTIES:
    1. Children come before parents
    2. Parent order is preserved from class definition
    3. Each class appears exactly once
    4. MRO must be consistent (monotonic)

    MONOTONICITY means: if class A precedes class B in the linearization of class C,
    then A must precede B in the linearization of any subclass of C (unless B is
    removed).
    """


    # ============================================================================
    # SECTION 2: C3 Algorithm - The Merge Process
    # ============================================================================

    """
    C3 LINEARIZATION ALGORITHM:

    For a class C with parents P1, P2, ..., Pn:

    L[C] = C + merge(L[P1], L[P2], ..., L[Pn], [P1, P2, ..., Pn])

    Where:
    - L[C] is the linearization (MRO) of class C
    - + means prepending C to the result
    - merge() is the C3 merge algorithm
    - [P1, P2, ..., Pn] is the list of parents in order

    MERGE ALGORITHM:
    1. Take the head of the first list
    2. If it's not in the tail of any other list, add it to result
    3. Otherwise, try the head of the next list
    4. Repeat until all lists are exhausted
    5. If we can't find a valid head, the hierarchy is invalid (inconsistent MRO)

    TERMINOLOGY:
    - Head: first element of a list
    - Tail: all elements except the first
    """


    def compute_c3_mro(cls, indent=0):
        """
        Manually compute C3 linearization with detailed steps.
        This is for educational purposes - Python does this automatically!
        """
        prefix = "  " * indent

        print(f"{prefix}Computing L[{cls.__name__}]:")

        # Base case: object
        if cls == object:
            print(f"{prefix}  Base case: object")
            return [object]

        # Get parents
        parents = cls.__bases__
        print(f"{prefix}  Parents: {[p.__name__ for p in parents]}")

        # Compute linearizations of parents
        parent_mros = []
        for parent in parents:
            print(f"{prefix}  Computing L[{parent.__name__}]:")
            mro = compute_c3_mro(parent, indent + 2)
            parent_mros.append(mro)
            print(f"{prefix}  L[{parent.__name__}] = {[c.__name__ for c in mro]}")

        # Prepare lists for merge
        lists_to_merge = parent_mros + [list(parents)]
        print(f"{prefix}  Lists to merge:")
        for i, lst in enumerate(lists_to_merge):
            print(f"{prefix}    {[c.__name__ for c in lst]}")

        # Perform merge
        print(f"{prefix}  Merging:")
        result = [cls] + c3_merge(lists_to_merge, prefix + "    ")

        print(f"{prefix}  Result: L[{cls.__name__}] = {[c.__name__ for c in result]}")
        return result


    def c3_merge(lists, prefix=""):
        """
        Perform C3 merge algorithm.
        """
        result = []

        while True:
            # Remove empty lists
            lists = [lst for lst in lists if lst]

            if not lists:
                break

            # Try to find a valid head
            found = False
            for lst in lists:
                head = lst[0]

                # Check if head is in tail of any list
                in_tail = False
                for other_list in lists:
                    if head in other_list[1:]:
                        in_tail = True
                        break

                if not in_tail:
                    # Valid head found!
                    result.append(head)
                    print(f"{prefix}Adding {head.__name__} (not in any tail)")

                    # Remove from all lists
                    for lst in lists:
                        if lst and lst[0] == head:
                            lst.pop(0)

                    found = True
                    break

            if not found:
                # Inconsistent MRO!
                print(f"{prefix}ERROR: Cannot find valid head - inconsistent MRO!")
                raise TypeError("Cannot create consistent MRO")

        return result


    # ============================================================================
    # SECTION 3: Step-by-Step Example - Simple Diamond
    # ============================================================================

    """
    Let's compute MRO for a simple diamond step by step.
    """


    class A:
        """Base class."""
        pass


    class B(A):
        """Left branch."""
        pass


    class C(A):
        """Right branch."""
        pass


    class D(B, C):
        """Diamond bottom."""
        pass


    print("="*70)
    print("C3 LINEARIZATION - SIMPLE DIAMOND")
    print("="*70)

    print("\nClass hierarchy:")
    print("     A")
    print("    / \\")
    print("   B   C")
    print("    \\ /")
    print("     D")

    print("\n" + "-"*70)
    print("MANUAL COMPUTATION:")
    print("-"*70)

    # Show the computation
    result = compute_c3_mro(D)

    print("\n" + "-"*70)
    print("PYTHON'S ACTUAL MRO:")
    print("-"*70)
    print(f"D.__mro__ = {[c.__name__ for c in D.__mro__]}")

    print("\nMatches our computation! ✓")


    # ============================================================================
    # SECTION 4: Another Example - Multiple Parents
    # ============================================================================

    """
    Computing MRO for a class with multiple unrelated parents.
    """


    class X:
        pass


    class Y:
        pass


    class Z:
        pass


    class Multi(X, Y, Z):
        """Class with three independent parents."""
        pass


    print("\n" + "="*70)
    print("C3 LINEARIZATION - MULTIPLE INDEPENDENT PARENTS")
    print("="*70)

    print("\nClass hierarchy:")
    print("X    Y    Z")
    print(" \\   |   /")
    print("  \\  |  /")
    print("   Multi")

    print("\n" + "-"*70)
    print("COMPUTING L[Multi]:")
    print("-"*70)

    print("""
    Step-by-step:
    1. L[Multi] = Multi + merge(L[X], L[Y], L[Z], [X, Y, Z])
    2. L[X] = [X, object]
    3. L[Y] = [Y, object]
    4. L[Z] = [Z, object]
    5. merge([X, object], [Y, object], [Z, object], [X, Y, Z])
       - Take X (not in any tail) → result: [X]
       - Take Y (not in any tail) → result: [X, Y]
       - Take Z (not in any tail) → result: [X, Y, Z]
       - Take object (not in any tail) → result: [X, Y, Z, object]
    6. L[Multi] = [Multi, X, Y, Z, object]
    """)

    print("Python's actual MRO:")
    print(f"Multi.__mro__ = {[c.__name__ for c in Multi.__mro__]}")


    # ============================================================================
    # SECTION 5: Complex Example - Nested Diamonds
    # ============================================================================

    """
    A more complex hierarchy with nested diamonds.
    """


    class O:
        """Root."""
        pass


    class A(O):
        pass


    class B(O):
        pass


    class C(O):
        pass


    class D(A, B):
        pass


    class E(B, C):
        pass


    class F(D, E):
        """
        Complex hierarchy:
              O
            / | \\
           A  B  C
           |  |\\|
           D  | E
            \\ |/
              F
        """
        pass


    print("\n" + "="*70)
    print("C3 LINEARIZATION - COMPLEX HIERARCHY")
    print("="*70)

    print("\nClass hierarchy:")
    print("       O")
    print("     / | \\")
    print("    A  B  C")
    print("    |  |\\|")
    print("    D  | E")
    print("     \\ |/")
    print("       F")

    print("\n" + "-"*70)
    print("COMPUTING L[F]:")
    print("-"*70)

    print("""
    Step-by-step (simplified):
    1. L[A] = [A, O, object]
    2. L[B] = [B, O, object]
    3. L[C] = [C, O, object]
    4. L[D] = [D, A, B, O, object]
    5. L[E] = [E, B, C, O, object]
    6. L[F] = F + merge([D, A, B, O, object], [E, B, C, O, object], [D, E])

    Detailed merge:
    - Take F → [F]
    - Take D (not in tail of [E, B, C, O, object] or [D, E]) → [F, D]
    - Take A (not in any tail) → [F, D, A]
    - Take B from first list? No, B is in tail of [E, B, C, O, object]
    - Take E (not in any tail) → [F, D, A, E]
    - Take B (now not in any tail) → [F, D, A, E, B]
    - Take C (not in any tail) → [F, D, A, E, B, C]
    - Take O (not in any tail) → [F, D, A, E, B, C, O]
    - Take object → [F, D, A, E, B, C, O, object]
    """)

    print("Python's actual MRO:")
    for i, cls in enumerate(F.__mro__, 1):
        print(f"{i}. {cls.__name__}")


    # ============================================================================
    # SECTION 6: Inconsistent MRO - Why Some Hierarchies Fail
    # ============================================================================

    """
    Some inheritance patterns cannot be linearized consistently.
    C3 will detect and reject these.
    """


    class P1:
        pass


    class P2:
        pass


    # This is OK
    class C1(P1, P2):
        """C1 says: P1 before P2"""
        pass


    # This is also OK
    class C2(P2, P1):
        """C2 says: P2 before P1 (opposite order)"""
        pass


    # But this will FAIL
    print("\n" + "="*70)
    print("INCONSISTENT MRO EXAMPLE")
    print("="*70)

    print("\nClass hierarchy:")
    print("P1    P2")
    print("| \\  / |")
    print("|  \\/  |")
    print("|  /\\  |")
    print("| /  \\ |")
    print("C1    C2")
    print(" \\    /")
    print("  \\  /")
    print("   Bad")

    print("\nC1 says: P1 before P2")
    print("C2 says: P2 before P1")
    print("Bad inherits from both C1 and C2")
    print("\nThis creates a conflict:")

    print("""
    L[Bad] = Bad + merge(L[C1], L[C2], [C1, C2])
           = Bad + merge([C1, P1, P2, object], [C2, P2, P1, object], [C1, C2])

    Try to merge:
    1. Take Bad → [Bad]
    2. Take C1 → [Bad, C1]
    3. Take P1? NO - P1 is in tail of [C2, P2, P1, object]
    4. Take C2 from [C2, P2, P1, object]? NO - C2 is in tail of [C1, C2]
    5. STUCK! Cannot find a valid head.

    This is an INCONSISTENT MRO - Python will raise TypeError.
    """)

    print("\nTrying to create the class:")
    try:
        class Bad(C1, C2):
            pass
        print("Success (unexpected!)")
    except TypeError as e:
        print(f"TypeError: {e}")
        print("\nPython correctly rejected this inconsistent hierarchy!")


    # ============================================================================
    # SECTION 7: Properties of Valid MRO
    # ============================================================================

    """
    A valid MRO must satisfy these properties:
    """


    def check_mro_properties(cls):
        """
        Check if MRO satisfies C3 properties.
        """
        mro = cls.__mro__

        print(f"\nChecking MRO properties for {cls.__name__}:")
        print(f"MRO: {[c.__name__ for c in mro]}")

        # Property 1: Class comes first
        print(f"\n1. Class comes first:")
        print(f"   {mro[0].__name__} == {cls.__name__}: {mro[0] == cls} ✓")

        # Property 2: Parents appear after children
        print(f"\n2. Parents appear after children:")
        for i, c in enumerate(mro[:-1]):  # Skip object
            for parent in c.__bases__:
                if parent in mro:
                    parent_index = mro.index(parent)
                    if parent_index > i:
                        print(f"   {c.__name__} (pos {i}) before {parent.__name__} (pos {parent_index}) ✓")
                    else:
                        print(f"   ERROR: {c.__name__} (pos {i}) after {parent.__name__} (pos {parent_index}) ✗")

        # Property 3: Each class appears once
        print(f"\n3. Each class appears exactly once:")
        if len(mro) == len(set(mro)):
            print(f"   All {len(mro)} classes are unique ✓")
        else:
            print(f"   ERROR: Duplicate classes found ✗")

        # Property 4: object is last
        print(f"\n4. object is last:")
        print(f"   Last class is {mro[-1].__name__}: {mro[-1] == object} ✓")

        # Property 5: Local precedence order
        print(f"\n5. Local precedence order preserved:")
        for c in mro[:-1]:
            if c.__bases__:
                print(f"   {c.__name__} has parents: {[p.__name__ for p in c.__bases__]}")
                for i, p1 in enumerate(c.__bases__[:-1]):
                    p2 = c.__bases__[i + 1]
                    if p1 in mro and p2 in mro:
                        if mro.index(p1) < mro.index(p2):
                            print(f"     {p1.__name__} before {p2.__name__} ✓")
                        else:
                            print(f"     ERROR: {p1.__name__} after {p2.__name__} ✗")


    print("\n" + "="*70)
    print("PROPERTIES OF VALID MRO")
    print("="*70)

    check_mro_properties(F)


    # ============================================================================
    # SECTION 8: Practical MRO Prediction
    # ============================================================================

    """
    Tips for predicting MRO:
    1. Start with the class itself
    2. Add parents left to right, recursing depth-first
    3. But skip parents that will appear later
    4. Add common ancestors only once at the end
    """


    def predict_mro(cls, show_steps=False):
        """
        Simple heuristic for predicting MRO (not always perfect).
        """
        if show_steps:
            print(f"\nPredicting MRO for {cls.__name__}:")

        # Start with the class itself
        result = [cls]

        # Process parents left to right
        for parent in cls.__bases__:
            if show_steps:
                print(f"  Processing parent: {parent.__name__}")

            # Get parent's MRO
            parent_mro = parent.__mro__

            # Add classes from parent's MRO that aren't already in result
            for c in parent_mro:
                if c not in result:
                    result.append(c)
                    if show_steps:
                        print(f"    Added: {c.__name__}")

        return result


    print("\n" + "="*70)
    print("MRO PREDICTION PRACTICE")
    print("="*70)

    # Test prediction
    predicted = predict_mro(F, show_steps=True)
    actual = F.__mro__

    print(f"\nPredicted: {[c.__name__ for c in predicted]}")
    print(f"Actual:    {[c.__name__ for c in actual]}")
    print(f"Match: {predicted == list(actual)} {('✓' if predicted == list(actual) else '✗')}")


    # ============================================================================
    # SECTION 9: Key Takeaways
    # ============================================================================

    """
    KEY POINTS ABOUT C3 LINEARIZATION:

    1. C3 Algorithm:
       - Merge parent linearizations with parent list
       - Take heads that aren't in any tail
       - Ensures consistent, predictable MRO
       - Rejects inconsistent hierarchies

    2. Properties Guaranteed:
       - Children before parents
       - Parent order preserved
       - Each class appears once
       - Monotonic (consistent with subclasses)
       - object always last

    3. Why Some Hierarchies Fail:
       - Conflicting parent orders
       - Cannot satisfy all constraints
       - Python raises TypeError
       - Fix by reordering parents or restructuring

    4. Computing MRO:
       - L[C] = C + merge(L[P1], ..., L[Pn], [P1, ..., Pn])
       - Merge takes valid heads only
       - Recursive process
       - Ends at object

    5. Practical Tips:
       - Design hierarchies carefully
       - Avoid conflicting parent orders
       - Test complex hierarchies
       - Use inspection tools
       - Document MRO intentions

    6. Historical Note:
       - Python 2.2 and earlier used different algorithm
       - C3 adopted in Python 2.3 (2003)
       - Solves diamond problem elegantly
       - Based on Dylan language's algorithm

    7. When You Need This:
       - Debugging complex hierarchies
       - Designing framework base classes
       - Understanding method resolution
       - Fixing TypeError for inconsistent MRO
       - Teaching advanced OOP concepts
    """

    print("\n" + "="*70)
    print("END OF C3 LINEARIZATION TUTORIAL")
    print("="*70)
    print("\nNext: Learn about complex real-world hierarchies!")