2
\$\begingroup\$

I am learning about user-defined exceptions in python and I have tried to incorporate them in a basic python implementation of a stack. Objects of Stack class support push, pop and top operations. I have defined the following exceptions:

  • StackEmptyError: Raise an exception when pop/top operation is performed on an empty stack.
  • InvalidStackOpError: Raise an exception when an invalid operation is attempted

Here is the code

class StackEmptyError(Exception):
    "Raised when pop/top operation is performed on empty stack"
    pass

class InvalidStackOpError(Exception):
    "Raised when an invalid stack operation is attempted"
    pass

class stack:
    def __init__(self):
        self.s = list()
        
    def push(self, x):
        self.s.append(x)
    
    def pop(self):
        if not len(self.s):
            raise StackEmptyError
        popped = self.s.pop()
        return popped
    
    def top(self):
        if not len(self.s):
            raise StackEmptyError
        return self.s[-1]
        
def perform_stackops(operations, values):
    st = stack()
    val_idx = 0
    for op in operations:
        if op == "push":
            st.push(values[val_idx])
            val_idx += 1
        elif op == "pop":
            print(f"Popped Element - {st.pop()}")
        elif op == "top":
            print(f"Top Element - {st.top()}")
        else:
            raise InvalidStackOpError(f"Operation {op} is invalid")


operations = ["push", "push", "pop", "pop", "push", "top"]
values = [20, 10, 30]

perform_stackops(operations, values)

Is this the right way to define and raise user-defined exceptions in python? Can I improve this even further?

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

"Is this the right way in Python?"

Is this the right way to define and raise user-defined exceptions in python?

The exception definitions are technically correct. when writing docstrings, it's recommended to enclose in """...""" instead of "...".

It would be good to add some tests that exercise the exceptions, because that's how you can really know that they are doing their job.

For more resources on using exceptions correctly I recommend:

  • The Python docs on the subject: 8. Errors and Exceptions (easy enough to find with web search for "python exceptions")
  • The Python style guide: PEP-8

Stack implementation

Class names should be PascalCase, so Stack would be more appropriate.

The attribute s is too short to be descriptive, stack would be better.

When initializing an empty list [] is recommended instead of list().

Instead of:

if not len(self.s):

The recommended writing style is simply:

if not self.s:

In this code:

popped = self.s.pop()
return popped

I don't think the popped variable adds much value, I would simply inline it (return self.s.pop()).

Write unit tests

It would be good to write unit tests that verify the stack works correctly, including the raising of exceptions.

Drop perform_stackops and InvalidStackOpError

I don't see much purpose for these elements. I think they just add unnecessary complexity in the program.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you! This is the feedback I was looking for. I did go through the "Errors and Exceptions" tutorial, but the section on "User-defined Exceptions" is very short and I wanted to know if my understanding is correct. \$\endgroup\$ Jun 23, 2023 at 20:55
3
\$\begingroup\$

About Raising and catching exceptions, about implementing a library and testing it. Answering your questions we have to discuss some high level concepts.

def perform_stackops(operations, values):

This function has unclear responsibility. If meant to act as guardian, it is a fail. Stack shall act as a grown up class. But most probably it is a test function only. As a test function

  • it is not clear why test cases are given as strings
  • it is not clear why it tries to uses an InvalidStackOpError As the test cases are given as strings, the function has troubles with invalid cases. However, invalid cases are not stack problems but problems of the test code. If throwing an Exception, this Exception relates to the test domain. On the other hand, there is the stack-domain with StackEmptyError and class.

Exceptions

... are made for catching. To allow specific catching we have to do Exception classes, some classes already defined. There is no need to define an Exception for every error, quite contrary, one should stick to the built-in ones if possible from the catching point of view. If your exception do not map to a built-in - roll your own.

So StackEmptyError could be replaced by IndexError("pop from empty stack") and IndexError("access top from empty stack").

In the test function the InvalidStackOpError could be represented as ValueError("invalid stack operation in test") or KeyError("invalid stack operation in test").

However your Exception definitions are correct. When throwing you do not differentiate whether pop() or top() did raise. Add some information.

checking vs catching

There are two options how to generate your exceptions

  • check before calling, then raise - this is what you did.
  • call in a try block, catch the underlying exception and raise with changed info (e.g. IndexError("pop from empty list") to IndexError("pop from empty stack")

The second one also works for black boxes, generators, ..., where you have no information beforehand.

Changed code

With above exception stuff and some renaming

class Stack:

    def __init__(self):
        self.data = []
        
    def push(self, x):
        self.data.append(x)
    
    def pop(self):
        if not self.data:
            raise IndexError("pop from empty stack")
        return self.data.pop()
    
    def top(self):
        if not self.data:
            raise IndexError("access top from empty stack")
        return self.data[-1]

further stuff for implementing good classes

For the external interface and exceptions have a look at existing python classes, in your case especially at list. Learn from those implementations. While not your target now, except for testing, have look at https://docs.python.org/3/reference/datamodel.html#basic-customization to allow your class to work flawlessly with external functions also.

testing

Have a look at https://docs.python.org/3/library/unittest.html There you learn how to test a class for expected behaviour including exception behaviour. For testing your class should at least implement __repr__ and __eq__.

Have fun

\$\endgroup\$
1
  • \$\begingroup\$ This is super helpful! \$\endgroup\$ Jun 23, 2023 at 22:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.