Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Comments on Use cases for raising a 'NotImplementedError' in Python

Parent

Use cases for raising a 'NotImplementedError' in Python

+6
−0

What are some legitimate use cases for raising a NotImplementedError exception in Python? How can it help express a code author's intentions to aid code maintenance and/or further development of code?

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

Post
+0
−1

One of the usecases I have found very useful is to do a raise NotImplementedError() inside the child method of an @abstractmethod-decorated base class method. Yes, it's a mouthful but what it really means is graceful simplicity.


Example

Imagine writing a control script for a family of electronic measurement modules (i.e. 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.

Many of the low-level commands 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 value 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!

You're right: NOT implementing set is not an option as Python would then fire the error below at you as soon as your as you tried instantiating your ADC object.

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

So you must implement set, because we made it an enforced verb (aka @abstractmethod), which is shared by two other modules. But, what should you implement if set does not make sense for this particular module? What makes the interface (for users) and the code-base (for future maintenance) as clean as possible?

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 this link about Bare exceptions for why this is important) (originally linked by TemporalWolf)
  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.
History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.

3 comment threads

If you want to allow not implementing set(self, channel), why would you declare it as required to imp... (1 comment)
Band-aid on bad design (1 comment)
TemporalWolf's missing link? (1 comment)
Band-aid on bad design
Derek Elkins‭ wrote 4 months ago

While I think something like this is relatively common, it's hardly a "graceful" solution. A better solution would be to adhere more closely to the Single Responsibility Principle and not combine reading and writing into a single interface. Instead, your abstract base class could be split into an Accessor class and an Actuator class. Then the ADC class could only inherit from Accessor, and you wouldn't need to have it lie about what methods it supports. Basically, you should only used NotImplementedError in this way if you are forced to by bad design in a third-party library. Otherwise, the need for NotImplementedError should be a huge red flag that there is something wrong with your design.

More generally, separating specifically reading and writing is often a powerful move. Read-only, write-only, and read-write interfaces all have fairly distinct properties. A function accepting only the read-only interface sends a very strong message about its behavior.