13

Does python have anything similar to a sealed class? I believe it's also known as final class, in java.

In other words, in python, can we mark a class so it can never be inherited or expanded upon? Did python ever considered having such a feature? Why?

Disclaimers

Actually trying to understand why sealed classes even exist. Answer here (and in many, many, many, many, many, really many other places) did not satisfy me at all, so I'm trying to look from a different angle. Please, avoid theoretical answers to this question, and focus on the title! Or, if you'd insist, at least please give one very good and practical example of a sealed class in csharp, pointing what would break big time if it was unsealed.

I'm no expert in either language, but I do know a bit of both. Just yesterday while coding on csharp I got to know about the existence of sealed classes. And now I'm wondering if python has anything equivalent to that. I believe there is a very good reason for its existence, but I'm really not getting it.

Community
  • 1
  • 1
cregox
  • 17,674
  • 15
  • 85
  • 116
  • Sealed classes are useful in a few cases. One might be that you have only static and/or const members so inheriting wouldn't help any. Another alternative use would be in a situation where the class is such that any errors ought to be fixed by changing the implementation and submitting the fix, rather than subclassing. `seal`ing the class could encourage that...although that's probably not how it was intended to be used ;) – Chris Pfohl May 15 '13 at 12:00
  • 1
    @ChristopherPfohl *only static / const* members sounds a good reason to seal it. But, again, who'd want to expand such a class anyway? Using it to enforce debugging only with you sounds like an awful centralized-oriented reason to me, but maybe the best reason I've heard so far. – cregox May 15 '13 at 13:09
  • 1
    looks in python 3.8 we got the Final decorator. https://www.python.org/dev/peps/pep-0591/ – KFL Feb 04 '22 at 02:34

5 Answers5

15

You can use a metaclass to prevent subclassing:

class Final(type):
    def __new__(cls, name, bases, classdict):
        for b in bases:
            if isinstance(b, Final):
                raise TypeError("type '{0}' is not an acceptable base type".format(b.__name__))
        return type.__new__(cls, name, bases, dict(classdict))

class Foo:
    __metaclass__ = Final

class Bar(Foo):
    pass

gives:

>>> class Bar(Foo):
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __new__
TypeError: type 'Foo' is not an acceptable base type

The __metaclass__ = Final line makes the Foo class 'sealed'.

Note that you'd use a sealed class in .NET as a performance measure; since there won't be any subclassing methods can be addressed directly. Python method lookups work very differently, and there is no advantage or disadvantage, when it comes to method lookups, to using a metaclass like the above example.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • This is awesome, and I believe it answers the question. Unfortunately, doesn't really help me at all, as I'd expect. This tells me the answer would actually be "no, python doesn't implement sealed classes, but you still can hack your own". But, would you know if this is a clean hack? You know, it won't break stuff or is highly disencourage or simply make the code runs slower... I believe originally, sealed classes are also meant to be faster. – cregox May 15 '13 at 12:56
  • @Cawas: It won't make anything go slower; the metaclass only applies to when you *define* a subclass. It won't make anything go faster either, there is no lookup-optimization that can be gleaned from using this 'hack'. – Martijn Pieters May 15 '13 at 12:58
  • Performance is just one of the minor yet simpler points people give for sealing classes. You did tell me how to seal a class in python, now I guess I just want to know why would you do it. Since it's not native, I suppose people never do it. And probably are advised against doing it. Why? – cregox May 15 '13 at 13:06
  • 1
    @Cawas: It certainly is not pythonic; preventing others from creating subclasses is a very authoritarian thing to do; part of the Python ethos is to let others just shoot themselves in the foot if they really want to, it's not the problem of the original class author. – Martijn Pieters May 15 '13 at 13:09
  • @PaulMcGuire: That is a good analogy. Like the private name mangling (names that start with a double-underscore), the `Final` metaclass can be circumvented but gives a powerful signal that the author was hoping not to have to deal with clashes with subclasses. :-) – Martijn Pieters May 15 '13 at 13:17
  • Erm... Who is PaulMcGuire and which analogy?! o_O – cregox May 15 '13 at 13:26
  • @Cawas: The comment Paul posted has been deleted since. It was an opionion that my answer was helpful as it showed a way to mark a class as final *by convention*, just as `_` before an attribute documents it as private. – Martijn Pieters May 15 '13 at 13:27
2

Before we talk Python, let's talk "sealed":

I, too, have heard that the advantage of .Net sealed / Java final / C++ entirely-nonvirtual classes is performance. I heard it from a .Net dev at Microsoft, so maybe it's true. If you're building a heavy-use, highly-performance-sensitive app or framework, you may want to seal a handful of classes at or near the real, profiled bottleneck. Particularly classes that you are using within your own code.

For most applications of software, sealing a class that other teams consume as part of a framework/library/API is kinda...weird.

Mostly because there's a simple work-around for any sealed class, anyway.

I teach "Essential Test-Driven Development" courses, and in those three languages, I suggest consumers of such a sealed class wrap it in a delegating proxy that has the exact same method signatures, but they're override-able (virtual), so devs can create test-doubles for these slow, nondeterministic, or side-effect-inducing external dependencies.

[Warning: below snark intended as humor. Please read with your sense of humor subroutines activated. I do realize that there are cases where sealed/final are necessary.]

The proxy (which is not test code) effectively unseals (re-virtualizes) the class, resulting in v-table look-ups and possibly less efficient code (unless the compiler optimizer is competent enough to in-line the delegation). The advantages are that you can test your own code efficiently, saving living, breathing humans weeks of debugging time (in contrast to saving your app a few million microseconds) per month... [Disclaimer: that's just a WAG. Yeah, I know, your app is special. ;-]

So, my recommendations: (1) trust your compiler's optimizer, (2) stop creating unnecessary sealed/final/non-virtual classes that you built in order to either (a) eke out every microsecond of performance at a place that is likely not your bottleneck anyway (the keyboard, the Internet...), or (b) create some sort of misguided compile-time constraint on the "junior developers" on your team (yeah...I've seen that, too).

Oh, and (3) write the test first. ;-)

Okay, yes, there's always link-time mocking, too (e.g. TypeMock). You got me. Go ahead, seal your class. Whatevs.

Back to Python: The fact that there's a hack rather than a keyword is probably a reflection of the pure-virtual nature of Python. It's just not "natural."

By the way, I came to this question because I had the exact same question. Working on the Python port of my ever-so-challenging and realistic legacy-code lab, and I wanted to know if Python had such an abominable keyword as sealed or final (I use them in the Java, C#, and C++ courses as a challenge to unit testing). Apparently it doesn't. Now I have to find something equally challenging about untested Python code. Hmmm...

Uncle Troy
  • 400
  • 1
  • 3
  • 6
  • very verbose and interesting point of view. i actually couldn't agree more that python classes should never be sealed, even while i still wouldn't know to pinpoint the reasoning behind - it's probably along the lines of "haven't even considered doing anything this authoritarian" thing. anyway, perhaps you could turn this into an awesome answer if you also add practical ways of shooting ourselves in the foot (as to answer the question in the title) if we want to while explaining why we shouldn't and the best alternatives. ;P – cregox Aug 11 '18 at 19:03
1

Python does have classes that can't be extended, such as bool or NoneType:

>>> class ExtendedBool(bool):
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type 'bool' is not an acceptable base type

However, such classes cannot be created from Python code. (In the CPython C API, they are created by not setting the Py_TPFLAGS_BASETYPE flag.)

Python 3.6 will introduce the __init_subclass__ special method; raising an error from it will prevent creating subclasses. For older versions, a metaclass can be used.

Still, the most “Pythonic” way to limit usage of a class is to document how it should not be used.

Petr Viktorin
  • 65,510
  • 9
  • 81
  • 81
1

Python 3.8 has that feature in the form of the typing.final decorator:

class Base:
    @final
    def done(self) -> None:
        ...
class Sub(Base):
    def done(self) -> None:  # Error reported by type checker
        ...

@final
class Leaf:
    ...
class Other(Leaf):  # Error reported by type checker

See https://docs.python.org/3/library/typing.html#typing.final

Grumbel
  • 6,585
  • 6
  • 39
  • 50
0

Similar in purpose to a sealed class and useful to reduce memory usage (Usage of __slots__?) is the __slots__ attribute which prevents monkey patching a class. Because when the metaclass __new__ is called, it is too late to put a __slots__ into the class, we have to put it into the namespace at the first possible timepoint, i.e. during __prepare__. Additionally, this throws the TypeError a little bit earlier. Using mcs for the isinstance comparison removes the necessity to hardcode the metaclass name in itself. The disadvantage is that all unslotted attributes are read-only. Therefore, if we want to set specific attributes during initialization or later, they have to slotted specifically. This is feasible e.g. by using a dynamic metaclass taking slots as an argument.

def Final(slots=[]):
    if "__dict__" in slots:
        raise ValueError("Having __dict__ in __slots__ breaks the purpose")
    class _Final(type):
        @classmethod
        def __prepare__(mcs, name, bases, **kwargs):   
            for b in bases:
                if isinstance(b, mcs):
                    msg = "type '{0}' is not an acceptable base type"
                    raise TypeError(msg.format(b.__name__))

            namespace = {"__slots__":slots}
            return namespace
    return _Final

class Foo(metaclass=Final(slots=["_z"])):
    y = 1    
    def __init__(self, z=1):       
        self.z = 1

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self, val:int):
        if not isinstance(val, int):
            raise TypeError("Value must be an integer")
        else:
            self._z = val                

    def foo(self):
        print("I am sealed against monkey patching")

where the attempt of overwriting foo.foo will throw AttributeError: 'Foo' object attribute 'foo' is read-only and attempting to add foo.x will throw AttributeError: 'Foo' object has no attribute 'x'. The limiting power of __slots__ would be broken when inheriting, but because Foo has the metaclass Final, you can't inherit from it. It would also be broken when dict is in slots, so we throw a ValueError in case. To conclude, defining setters and getters for slotted properties allows to limit how the user can overwrite them.

foo = Foo()
# attributes are accessible
foo.foo()
print(foo.y)
# changing slotted attributes is possible
foo.z = 2

# %%
# overwriting unslotted attributes won't work
foo.foo = lambda:print("Guerilla patching attempt")
# overwriting a accordingly defined property won't work
foo.z = foo.foo
# expanding won't work
foo.x = 1
# %% inheriting won't work
class Bar(Foo):
    pass

In that regard, Foo could not be inherited or expanded upon. The disadvantage is that all attributes have to be explicitly slotted, or are limited to a read-only class variable.