42

This question is built on top of many assumptions. If one assumption is wrong, then the whole thing falls over. I'm still relatively new to Python and have just entered the curious/exploratory phase.

It is my understanding that Python does not support the creating of classes that cannot be subclassed (final classes). However, it seems to me that the bool class in Python cannot be subclassed. This makes sense when the intent of the bool class is considered (because bool is only supposed to have two values: true and false), and I'm happy with that. What I want to know is how this class was marked as final.

So my question is: how exactly did Guido manage to prevent subclassing of bool?

>>> class TestClass(bool):
        pass

Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    class TestClass(bool):
TypeError: type 'bool' is not an acceptable base type

Related question: Why I can't extend bool in Python?

Community
  • 1
  • 1
GlenCrawford
  • 3,359
  • 3
  • 26
  • 34

4 Answers4

55

You can simulate the same effect from Python 3.x quite easily:

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 C(metaclass=Final): pass

class D(C): pass

will give the following output:

Traceback (most recent call last):
  File "C:\Temp\final.py", line 10, in <module>
    class D(C): pass
  File "C:\Temp\final.py", line 5, in __new__
    raise TypeError("type '{0}' is not an acceptable base type".format(b.__name__))
TypeError: type 'C' is not an acceptable base type
Duncan
  • 92,073
  • 11
  • 122
  • 156
19

You could do this only via the C API. Clear the Py_TPFLAGS_BASETYPE bit of the tp_flags of the type object.

As example, bool cannot be subclassed (cpython-code on github):

PyTypeObject PyBool_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "bool",
    ...
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,                         /* tp_flags */
    ...
};

but int can (cpython-code on github):

PyTypeObject PyLong_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",                                      /* tp_name */
    ...
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
        Py_TPFLAGS_LONG_SUBCLASS,               /* tp_flags */
    ...
};

because Py_TPFLAGS_BASETYPE-bit is set in tp_flags.

ead
  • 32,758
  • 6
  • 90
  • 153
kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
  • Link to documentation: http://docs.python.org/c-api/typeobj.html#Py_TPFLAGS_BASETYPE – Denis Otkidach May 13 '10 at 08:56
  • 1
    While the accepted answer is super neat, this should be accepted for actually answering the question, `"how exactly did Guido manage to prevent subclassing of bool?"` – ddejohn Sep 16 '22 at 19:09
16

You should block subclassing without using a metaclass like this:

class SomeBase:

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if cls is not SomeBase:
            raise TypeError("SomeBase does not support polymorphism.  Use composition over inheritance.")


class Derived(SomeBase):
    pass

In Python 3.8, you should also use the final decorator to induce type-checking errors:

from typing import final


@final
class SomeBase:
    ...

Type-checking is done by programs like MyPy, which are optional.

Neil G
  • 32,138
  • 39
  • 156
  • 257
5

Final and @final types are now available in typing_extensions.

I wrote an article covering almost every part of this new type: https://sobolevn.me/2018/07/real-python-contants

Some examples with classes:

from typing_extensions import final

@final
class HRBusinessUnit(AbstractBusinessUnit):
    def grant_permissions(self) -> None:
        self.api.do_some_hr_stuff()


class SubHRBusinessUnit(HRBusinessUnit):  # mypy will raise an error
    def grant_permissions(self) -> None:
        self.api.do_some_it_stuff()

And with constants:

from typing_extensions import Final

DAYS_IN_A_WEEK: Final = 7
DAYS_IN_A_WEEK = 8  # mypy will raise an error

Also we have a small library to write final classes that are also checked at runtime! https://github.com/wemake-services/final-class

from final_class import final


@final
class Example(object):  # You won't be able to subclass it!
    ...


class Error(Example):  # Raises `TypeError`
    ...

Features:

  • No metaclass conflicts
  • No runtime overhead
  • No dependencies
  • Type hints included
  • Designed to be as simple as possible
sobolevn
  • 16,714
  • 6
  • 62
  • 60
  • 1
    Installing a whole library that just provides a decorator to set `__init_subclass__` is over-engineering. By the way a simple `Example.__init_subclass__ = lambda: None` makes `Example` subclassable again. – Richard Neumann Sep 07 '18 at 15:06
  • @RichardNeumann There's no clean way in Python to make a final class that is not in some way undoable in a derived class. So, your second statement is a fact, but it's irrelevant. `__init_subclass__` should categorically call super. – Neil G Sep 07 '18 at 16:11
  • 1
    `typing.final` is not the same thing, it's documentation on steriods, a static analysis tool, not a run-time-enforced limit on subclassing. That's not really a 'feature', as a *different use-case*. – Martijn Pieters Feb 10 '20 at 20:52
  • > Also we have a small library to write final classes that are also checked at runtime! https://github.com/wemake-services/final-class – sobolevn Feb 12 '20 at 06:42