Python Final: Preventing Inheritance and Override

Python's dynamic nature and philosophy of treating developers as 'consenting adults' means it traditionally lacks hard restrictions on inheritance and method overriding. Unlike Java's `final` keyword...

Key Insights

  • Python’s @final decorator from the typing module is a type-checking hint, not a runtime enforcement mechanism—mypy and pyright will flag violations, but Python itself won’t stop inheritance or overrides at runtime
  • You can implement true runtime finality using __init_subclass__ hooks or custom metaclasses, though this goes against Python’s “consenting adults” philosophy of trusting developers
  • Before reaching for @final, consider whether composition over inheritance or clear documentation might better serve your design goals without restricting extensibility

Introduction to Finality in Python

Python’s dynamic nature and philosophy of treating developers as “consenting adults” means it traditionally lacks hard restrictions on inheritance and method overriding. Unlike Java’s final keyword or C++’s final specifier, Python doesn’t prevent you from subclassing any class or overriding any method—at least not by default.

But sometimes you genuinely need to prevent inheritance. Maybe you’ve made performance optimizations that assume a class won’t be subclassed. Perhaps you’re implementing a security-sensitive component where subclassing could bypass validation. Or you might be designing a library API where allowing inheritance would create maintenance nightmares.

Consider this problematic scenario:

class SecureConnection:
    def __init__(self, credentials):
        self._validate_credentials(credentials)
        self.credentials = credentials
    
    def _validate_credentials(self, creds):
        # Complex security validation
        if not self._check_signature(creds):
            raise SecurityError("Invalid credentials")
    
    def _check_signature(self, creds):
        # Cryptographic verification
        return verify_signature(creds)

# Someone creates a subclass that bypasses security
class InsecureConnection(SecureConnection):
    def _check_signature(self, creds):
        return True  # Oops! Security bypassed

This is where Python’s @final decorator becomes useful.

The @final Decorator for Classes

Python 3.8 introduced the @final decorator in the typing module. When applied to a class, it signals that the class should not be subclassed:

from typing import final

@final
class ConfigurationManager:
    """Singleton configuration manager that must not be subclassed."""
    
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def get_config(self, key: str) -> str:
        return self._config.get(key, "")

If you attempt to inherit from this class, type checkers will complain:

# Type checker error: Cannot inherit from final class
class CustomConfig(ConfigurationManager):
    pass

Real-world use cases for final classes include:

Data Transfer Objects (DTOs): Classes that represent data structures where subclassing would complicate serialization:

from typing import final
from dataclasses import dataclass

@final
@dataclass
class UserDTO:
    """User data transfer object for API responses."""
    id: int
    username: str
    email: str
    
    def to_json(self) -> dict:
        # Serialization logic assumes exact structure
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email
        }

Value Objects: Immutable objects where identity is based on value, not reference:

from typing import final

@final
class Money:
    """Immutable money value object."""
    
    def __init__(self, amount: float, currency: str):
        self._amount = amount
        self._currency = currency
    
    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return (self._amount == other._amount and 
                self._currency == other._currency)
    
    def __hash__(self):
        return hash((self._amount, self._currency))

The @final Decorator for Methods

You can also mark individual methods as final to prevent overriding in subclasses while still allowing the class itself to be inherited:

from typing import final
from abc import ABC, abstractmethod

class DataProcessor(ABC):
    @abstractmethod
    def process(self, data: str) -> str:
        """Subclasses must implement data processing."""
        pass
    
    @final
    def validate_and_process(self, data: str) -> str:
        """Template method that must not be overridden."""
        if not data:
            raise ValueError("Data cannot be empty")
        
        if len(data) > 1000000:
            raise ValueError("Data too large")
        
        return self.process(data)

class JSONProcessor(DataProcessor):
    def process(self, data: str) -> str:
        return json.dumps(json.loads(data), indent=2)
    
    # Type checker error: Cannot override final method
    def validate_and_process(self, data: str) -> str:
        return self.process(data)  # Bypasses validation!

This pattern is particularly useful for template methods where you want subclasses to customize behavior through specific hooks while keeping the overall algorithm intact.

Type Checkers vs Runtime Behavior

Here’s the crucial limitation: @final is purely a type-checking hint. Python’s runtime completely ignores it:

from typing import final

@final
class FinalClass:
    def greet(self):
        return "Hello from FinalClass"

# Mypy error: Cannot inherit from final class "FinalClass"
class SubClass(FinalClass):
    def greet(self):
        return "Hello from SubClass"

# But at runtime, this works perfectly fine:
obj = SubClass()
print(obj.greet())  # Outputs: Hello from SubClass

Run this with mypy:

$ mypy example.py
example.py:7: error: Cannot inherit from final class "FinalClass"

But execute it with Python:

$ python example.py
Hello from SubClass

This disconnect between static analysis and runtime behavior is intentional. Python’s philosophy is that type hints are optional tools for developers, not runtime constraints. The @final decorator helps catch design violations during development and code review, but doesn’t enforce them.

Implementing Runtime Final Enforcement

If you genuinely need runtime enforcement, you can implement it using __init_subclass__:

class FinalMeta(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        for base in bases:
            if isinstance(base, FinalMeta):
                raise TypeError(f"Cannot inherit from final class {base.__name__}")
        return super().__new__(mcs, name, bases, namespace, **kwargs)

class FinalClass(metaclass=FinalMeta):
    def greet(self):
        return "Hello from FinalClass"

# This will raise TypeError at class definition time
try:
    class SubClass(FinalClass):
        pass
except TypeError as e:
    print(f"Error: {e}")  # Error: Cannot inherit from final class FinalClass

A simpler approach using __init_subclass__:

from typing import final

@final
class RuntimeFinalClass:
    def __init_subclass__(cls, **kwargs):
        raise TypeError(
            f"Cannot inherit from final class {cls.__bases__[0].__name__}"
        )
    
    def process(self):
        return "Processing..."

# Raises TypeError immediately
class Subclass(RuntimeFinalClass):
    pass

For method-level enforcement, you can create a custom decorator:

def runtime_final(method):
    """Decorator that enforces finality at runtime."""
    method.__final__ = True
    return method

class EnforcedFinalMeta(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        for base in bases:
            for attr_name in dir(base):
                base_attr = getattr(base, attr_name)
                if hasattr(base_attr, '__final__') and attr_name in namespace:
                    raise TypeError(
                        f"Cannot override final method {attr_name} "
                        f"from {base.__name__}"
                    )
        return super().__new__(mcs, name, bases, namespace, **kwargs)

class BaseClass(metaclass=EnforcedFinalMeta):
    @runtime_final
    def critical_method(self):
        return "Cannot override this"
    
    def normal_method(self):
        return "Can override this"

# This raises TypeError
try:
    class SubClass(BaseClass):
        def critical_method(self):
            return "Trying to override"
except TypeError as e:
    print(f"Prevented: {e}")

Best Practices and Alternatives

Before using @final, consider whether you really need it. Python’s flexibility is a feature, not a bug. Here are guidelines:

Use @final when:

  • Performance optimizations depend on implementation details
  • Security requires preventing subclass manipulation
  • You’re designing a library API and need to reserve the right to change internals
  • The class represents a value object or DTO with specific serialization requirements

Consider alternatives when:

  • You’re just trying to communicate intent (documentation might suffice)
  • The restriction is about code organization, not correctness
  • You’re working on application code rather than library code

Composition often provides better flexibility than final classes:

# Instead of this:
@final
class DataValidator:
    def validate(self, data):
        # validation logic
        pass

# Consider this:
class DataValidator:
    def validate(self, data):
        # validation logic
        pass

class DataProcessor:
    def __init__(self, validator: DataValidator):
        self._validator = validator
    
    def process(self, data):
        self._validator.validate(data)
        # processing logic

Avoid over-engineering with @final:

# Probably overkill:
@final
class StringHelper:
    @staticmethod
    def capitalize(s: str) -> str:
        return s.capitalize()

# Just use a function:
def capitalize(s: str) -> str:
    return s.capitalize()

Conclusion

Python’s @final decorator provides a way to express design intent and catch inheritance violations during static analysis. It’s particularly valuable in library code where you need to maintain API stability or in security-sensitive contexts where subclassing could bypass protections.

However, remember that @final is a type-checking hint, not a runtime constraint. If you need true enforcement, you’ll need to implement it yourself using metaclasses or __init_subclass__ hooks—but consider whether this aligns with your project’s philosophy and whether simpler alternatives like composition or clear documentation might better serve your needs.

The key is balance: use @final when it genuinely improves your codebase’s maintainability and correctness, but don’t let it become a tool for over-constraining code that benefits from Python’s flexibility. As with many Python features, the best approach depends on your specific context, team conventions, and the nature of the code you’re writing.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.