Exploiting Python metaclasses to forbid subclassing and where it fails

February 19, 2015

Suppose, entirely hypothetically and purely for illustration, that you want to create a class that cannot be subclassed. Python is normally a very permissive language, but as I mentioned in passing yesterday you can abuse some metaclass features to do this. Well, to more or less do this; as it happens there is a way around it because Python allows callables to be specified as your metaclass.

While there are complex approaches with a single metaclass that has a sophisticated __new__, it's easier to illustrate the whole idea like this (in Python 3 for once):

class Block(type):
  def __new__(meta, cname, bases, cdict):
    raise TypeError("cannot subclass")

def metacls(cname, bases, cdict):
  return type.__new__(Block, cname, bases, cdict)

class A(metaclass=metacls):
  pass

This works basically by cheating; our callable metaclass gives the newly created class a different metaclass that blocks the creation of further classes in its __new__. So let's try to get tricky to get around this. Our first attempt is to give our new subclass a different metaclass that doesn't block class creation:

class MNothing(type):
  pass

class B(A, metaclass=MNothing):
  pass

This will fail:

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

But wait, we don't actually need a new class here. We just need something that allows class creation but gives the new class the metaclass it needs in order to not have a metaclass conflict. As it happens we already have that:

class B(A, metaclass=metacls):
  pass

(note that if we didn't have metacls(), creating it is trivial. type(A) will give us the metaclass class we need to shim in.)

This not only works, this is the simple and more or less ultimate bypass for anything that tries to block __new__ because using a callable as the metaclass splits apart what happens to a class as it's being created from the class's metaclass class. The new class's official post-creation metaclass gets no chance to intervene during creation; it just winds up being the metaclass of a new class by fiat, with nothing to say about it. Since the metaclass's __init__ (if any) doesn't get called here, the first our Block metaclass can possibly know about the new class is when the new class is used for something (attribute lookup, instance creation, or subclassing).

All of which goes to show that Python is permissive after all if you know the right tricks, even if it looks restrictive initially.

(Well, at least here.)

Written on 19 February 2015.
« I wish Python didn't allow any callable to be a 'metaclass'
All of our Solaris 10 machines are now out of production »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Thu Feb 19 01:30:01 2015
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.