I want to demonstrate how python inner classes work. In general, there aren't very many use cases for inner classes, which is why you don't see them very often. However, sometimes, they're a great solution to a problem.

I recently had a method which took as an argument, an integer telling the method what to do.

In [53]:
import enum
from typing import Tuple

class Outer(object):

    class Inner(enum.IntEnum):
        RED=1
        YELLOW=2
        GREEN=3


    # Constructor for class Outer
    def __init__(self, factor: int) -> None:
        self.factor = factor
        # self.Inner = self.Inner()
    
    # If I was not using enums, then this is how I code up the method, and it would still work (and fail) in exactly the same way. But it's brittle - it's sensitive to changes - and will break if somebody makes a change in the caller
    
    def method_1 (self, cmd: int ) -> Tuple:
        result_0 = cmd * self.factor
        if cmd == 1:
            result_1 = "RED: "
        elif cmd == 2:
            result_1 = "YELLOW: "
        elif cmd == 3:
            result_1 = "GREEN: "
        else:
            raise ValueError(f"Method_1 got a bad cmd: {cmd} ")
        return result_0, result_1
 
    # Here, I am using an inner class, which has enumerated values in it.
    # This is much more robust in the face of programming errors. I change
    # the type hint of the 2nd argument to method_2 to be Inner (a class)
    # instead of int, then pycharm (a very good IDE) immediately tells me that I am going to have an attribute error at runtime.

    # cmd is an Inner, which is a subclass of enum, so to get the value out of
    # it, I can reference the value attribute. I can also get the name of the
    # enum, so that's one less relationship I have to keep track of.
    
    def method_2 ( self, cmd: Outer.Inner ) -> Tuple:
        result_0 = cmd.value * self.factor 
        result_1 = cmd.name + ": "
        return result_0, result_1 
    

Now, it's time to run a program that uses classes Outer and Inner, and let's see what if happens:

In [54]:
if "__main__" == __name__:

    FACTOR = 2

    outer = Outer(factor=FACTOR)

    RED = outer.Inner.RED
    YELLOW = outer.Inner.YELLOW
    GREEN = outer.Inner.GREEN

    result = outer.method_1(1)
    assert result[0] == 1 * FACTOR, f"Result is {result}, but should have been {1*FACTOR}."
    print(f"{result[1]} (as an int) worked on method 1")

    result = outer.method_2(RED)
    assert result[0] == RED * FACTOR, f"Result is {result}, but should have been {RED.value * FACTOR}."
    print(f"{result[1]} (as an Inner) worked on method 2")

    result = outer.method_1(2)
    assert result[0] == 2 * FACTOR, f"Result is {result}, but should have been {2*FACTOR}."
    print(f"{result[1]} (as an int) worked on method 1")

    result = outer.method_2(YELLOW)
    assert result[0] == YELLOW.value * FACTOR, f"Result is {result}, but should have been {YELLOW.value * FACTOR}."
    print(f"{result[1]} (as an Inner) worked on method 2")

    result = outer.method_1(3)
    assert result[0] == 3 * FACTOR, f"Result is {result}, but should have been {3*FACTOR}."
    print(f"{result[1]} (as an int) worked on method_1")

    result = outer.method_2(GREEN)
    assert result[0] == GREEN.value * FACTOR,  f"Result is {result}, but should have been {GREEN.value * FACTOR}."
    print(f"{result[1]} (as an Inner) worked on method_2")
RED:  (as an int) worked on method 1
RED:  (as an Inner) worked on method 2
YELLOW:  (as an int) worked on method 1
YELLOW:  (as an Inner) worked on method 2
GREEN:  (as an int) worked on method_1
GREEN:  (as an Inner) worked on method_2

So far, so good. But now I am going to throw a wrench into the works. I can pass method_1 an integer. Type hinting won't detect a problem, but it's still wrong. By way of contrast, the type hinting will detect that WRENCH is not an Inner

In [55]:
    WRENCH = 341
    try:
        result = outer.method_1(WRENCH)
    except ValueError as v:
        print("outer.method_1 raised a ValueError exception, as predicted\n" + str(v) )
    else:
        assert result[0] == WRENCH * FACTOR
        print(f"{result[1]} (as an int) worked")

    try:
        WRENCH = outer.Inner.WRENCH
        result = outer.method_2(WRENCH)
    except ValueError as v:
        print("outer.method_1 raised a ValueError exception, which was one prediction\n" + str(v) )
    except AttributeError as a:
        print("outer.method_1 raised an AttributeError exception, which was the other prediction\n" + str(a) )
    else:
        assert result[0] == WRENCH.value * FACTOR
        print(f"{result[1]} (as an int) worked")
    
    print("The program ended normally")
    
outer.method_1 raised a ValueError exception, as predicted
Method_1 got a bad cmd: 341 
outer.method_1 raised an AttributeError exception, which was the other prediction
WRENCH
The program ended normally
In [ ]: