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.
Use cases for raising a 'NotImplementedError' in Python
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?
3 answers
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:
- You are protecting a user from erroneously issuing a command ('set') that is not (and shouldn't!) be implemented for this module.
- You are telling them explicitly what the problem is (see this link about Bare exceptions for why this is important) (originally linked by TemporalWolf)
- 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.
NotImplementedError
should generally be viewed as indicating some design problem. You should not be reaching for it as a matter of course.
Here are some potential times you might feel a desire to reach for NotImplementedError
.
-
During development, you want to test some code that is partially implemented. Using
NotImplementedError
for the parts not yet implemented would be fine. Of course, these uses ofNotImplementedError
should be removed before you "release" the project. -
A third-party library requires you to implement all methods of a class to use its functionality, but some methods don't make sense for your use-case. If the documentation states that some methods don't need to be implemented, then you'd be fine (but they'd probably already have default implementations that raised
NotImplementedError
). If not, then you don't know if it is safe to not implement one of the methods. UsingNotImplementedError
in this case should be viewed as a hack that may well cause unknown issues now and/or in the future. Arguably, being in this situation either way indicates poor design in the third party library. -
You have a plugin system and a plugin may optionally provide certain functionality.
NotImplementedError
may seem like a good way to handle optional functionality that's not provided, but a better way is to query the plugin for different facets (or bundles) of coherent functionality. For example, if you had an IDE plugin that could support syntax highlighting or jump to definition among other things, you could havegetSyntaxHighlighterFacet()
andgetJumpToDefinitionFacet()
methods that the plugin must implement. These methods would return a coherent object if it supports that functionality, otherwise it would returnNone
. This would allow the plugin loader to query what functionality is available and substitute a fallback for functionality that's not available. It would also avoid finding out some functionality is unavailable at the last possible moment where it might lead to unexpected behavior or require fallback logic to be written everywhere.
This is in the docs. To paraphrase:
- Used for abstract methods that must be overridden in subclasses
- When the implementation is still WIP, but you want to leave a placeholder for the method name and signature
It's a way to to "reserve namespace", but with the actual method body "coming soon" (from you or someone else).
0 comment threads