[python] When to use 'raise NotImplementedError'?

One could also do a raise NotImplementedError() inside the child method of an @abstractmethod-decorated base class method.


Imagine writing a control script for a family of measurement modules (physical devices). The functionality of each module is narrowly-defined, implementing just one dedicated function: one could be an array of relays, another a multi-channel DAC or ADC, another an ammeter etc.

Much of the low-level commands in use would be shared between the modules for example to read their ID numbers or to send a command to them. Let's see what we have at this point:

Base Class

from abc import ABC, abstractmethod  #< we'll make use of these later

class Generic(ABC):
    ''' Base class for all measurement modules. '''

    # Shared functions
    def __init__(self):
        # do what you must...

    def _read_ID(self):
        # same for all the modules

    def _send_command(self, value):
        # same for all the modules

Shared Verbs

We then realise that much of the module-specific command verbs and, therefore, the logic of their interfaces is also shared. Here are 3 different verbs whose meaning would be self-explanatory considering a number of target modules.

  • get(channel)

  • relay: get the on/off status of the relay on channel

  • DAC: get the output voltage on channel

  • ADC: get the input voltage on channel

  • enable(channel)

  • relay: enable the use of the relay on channel

  • DAC: enable the use of the output channel on channel

  • ADC: enable the use of the input channel on channel

  • set(channel)

  • relay: set the relay on channel on/off

  • DAC: set the output voltage on channel

  • ADC: hmm... nothing logical comes to mind.


Shared Verbs Become Enforced Verbs

I'd argue that there is a strong case for the above verbs to be shared across the modules as we saw that their meaning is evident for each one of them. I'd continue writing my base class Generic like so:

class Generic(ABC):  # ...continued
    
    @abstractmethod
    def get(self, channel):
        pass

    @abstractmethod
    def enable(self, channel):
        pass

    @abstractmethod
    def set(self, channel):
        pass

Subclasses

We now know that our subclasses will all have to define these methods. Let's see what it could look like for the ADC module:

class ADC(Generic):

    def __init__(self):
        super().__init__()  #< applies to all modules
        # more init code specific to the ADC module
    
    def get(self, channel):
        # returns the input voltage measured on the given 'channel'

    def enable(self, channel):
        # enables accessing the given 'channel'

You may now be wondering:

But this won't work for the ADC module as set makes no sense there as we've just seen this above!

You're right: not implementing set is not an option as Python would then fire the error below when you tried to instantiate your ADC object.

TypeError: Can't instantiate abstract class 'ADC' with abstract methods 'set'

So you must implement something, because we made set an enforced verb (aka '@abstractmethod'), which is shared by two other modules but, at the same time, you must also not implement anything as set does not make sense for this particular module.

NotImplementedError to the Rescue

By completing the ADC class like this:

class ADC(Generic): # ...continued

    def set(self, channel):
        raise NotImplementedError("Can't use 'set' on an ADC!")

You are doing three very good things at once:

  1. You are protecting a user from erroneously issuing a command ('set') that is not (and shouldn't!) be implemented for this module.
  2. You are telling them explicitly what the problem is (see TemporalWolf's link about 'Bare exceptions' for why this is important)
  3. You are protecting the implementation of all the other modules for which the enforced verbs do make sense. I.e. you ensure that those modules for which these verbs do make sense will implement these methods and that they will do so using exactly these verbs and not some other ad-hoc names.