midgard

Turn inner classes into subclasses

A decorator for Python classes that have inner classes, useful for sum types with data

Ever wanted to have inner classes in Python that are also subclasses of the outer class? I suppose you’re more likely to be wondering why anyone would want inner classes in Python in the first place. Actually, the combination is pretty neat to make sum types that support arguments and can be used in type hints and with instanceof!

Normally, an inner class can’t be made a subclass of its outer class, since that name just isn’t in scope. I figured I’d just override the __base__ class as suggested on StackOverflow, however that code fails if you eliminate the ugly T placeholder class.

So I put together something where you can hide all the ugliness in a file you never look at. All that’s left in your code, is a pretty decorator that makes all the inner classes also a subclass!

def inner_classes_are_subclasses(outercls):
	"""
    @author https://sr.ht/~midgard/
    @license CC0-1.0
    """
    for name, innercls in outercls.__dict__.items():
        if name.startswith("_") or not isinstance(innercls, type):
            continue
        setattr(
            outercls,
            name,
            type(
                innercls.__name__,
                tuple(filter(lambda x: x is not object, innercls.__bases__)) + (outercls,),
                dict(innercls.__dict__)
            )
        )
    return outercls

You can use it like so:

@inner_classes_are_subclasses
class Decision:
    @dataclass
    class Flag:
        message: str

    @dataclass
    class KeepBefore:
        pass

    @dataclass
    class KeepAfter:
        pass

    @dataclass
    class ModifyElement:
        tags: Dict[str, str]

    @dataclass
    class Delete:
        pass


# Toy example function
def decide(before_edits: osm_api.Node, after_edits: osm_api.Node) -> Decision:
    if before_edits.coords != after_edits.coords:
        return Decision.KeepBefore()

    return Decision.ModifyElement(tags={
        k: v
        for k, v in after_edits.tags
        if not (has_obscenity(k) or has_obscenity(v))
    })

inner_classes_are_subclasses is not just a toy feature! I use this in production code for my OpenStreetMap selective reverting script.

If you’re going to use this, feel free to leave a comment in my public inbox.