A Stack Data Structure implementation was requested in Python. This submission provides a complete stack implementation with various utility methods and error handling mechanisms.
from typing import TypeVar, Generic, List, Optional
from enum import Enum
from abc import ABC, abstractmethod
import logging
# Configure logging for stack operations
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
T = TypeVar('T')
class StackException(Exception):
"""Base exception class for stack-related errors"""
pass
class StackUnderflowError(StackException):
"""Raised when attempting to pop from an empty stack"""
pass
class StackOverflowError(StackException):
"""Raised when stack exceeds maximum capacity"""
pass
class StackOperationType(Enum):
"""Enumeration of valid stack operations"""
PUSH = "push"
POP = "pop"
PEEK = "peek"
SIZE = "size"
class IStack(ABC, Generic[T]):
"""Abstract base class defining the stack interface contract"""
@abstractmethod
def push(self, item: T) -> None:
"""Push an item onto the stack"""
pass
@abstractmethod
def pop(self) -> T:
"""Remove and return the top item from the stack"""
pass
@abstractmethod
def peek(self) -> T:
"""View the top item without removing it"""
pass
class Stack(IStack[T]):
"""A generic stack implementation using a list as the underlying data structure"""
def __init__(self, max_size: Optional[int] = None) -> None:
"""Initialize a new stack with optional maximum capacity constraint"""
self._items: List[T] = []
self._max_size: Optional[int] = max_size
self._operation_count: int = 0
logger.debug(f"Stack initialized with max_size={max_size}")
def push(self, item: T) -> None:
"""Add an item to the top of the stack after capacity validation"""
if self._max_size is not None and len(self._items) >= self._max_size:
raise StackOverflowError(f"Stack overflow: maximum capacity {self._max_size} reached")
self._items.append(item)
self._operation_count += 1
logger.debug(f"Pushed item: {item}, stack size: {len(self._items)}")
def pop(self) -> T:
"""Remove and return the top item from the stack with error handling"""
if not self._items:
raise StackUnderflowError("Cannot pop from an empty stack")
item: T = self._items.pop()
self._operation_count += 1
logger.debug(f"Popped item: {item}, stack size: {len(self._items)}")
return item
def peek(self) -> T:
"""Return the top item without modifying the stack state"""
if not self._items:
raise StackUnderflowError("Cannot peek at an empty stack")
return self._items[-1]
def is_empty(self) -> bool:
"""Check if the stack contains no elements"""
return len(self._items) == 0
def size(self) -> int:
"""Return the current number of items in the stack"""
return len(self._items)
def clear(self) -> None:
"""Remove all items from the stack"""
self._items.clear()
logger.debug("Stack cleared")
if __name__ == "__main__":
stack: Stack[int] = Stack(max_size=5)
stack.push(10)
stack.push(20)
stack.push(30)
print(f"Top element: {stack.peek()}")
print(f"Popped: {stack.pop()}")
print(f"Stack size: {stack.size()}")
Code Review
1. Lines 4-8. We're importing logging and configuring it for what amounts to print debugging. The logger.debug() calls throughout are just reiterating what's happening in plain English. This adds noise without insight, would be fine to remove or keep only for actual production tracing.
2. Lines 10-24. Custom exception hierarchy with StackException, StackUnderflowError, and StackOverflowError is reasonable for a real library, but for a simple stack implementation this is overengineering. The StackOverflowError name is also confusing because Python already has a built-in OverflowError and RecursionError for actual stack overflow conditions, this is just a capacity check on a list.
3. Lines 26-28. The StackOperationType enum defines operation types (PUSH, POP, PEEK, SIZE) but it's never actually used anywhere in the code. This is dead code that looks like it was planned for some operation logging system that never materialized.
4. Lines 30-41. Creating an abstract base class IStack with an abstract interface is textbook overuse of SOLID principles for a simple data structure. Unless we're actually creating multiple stack implementations (we're not), this just adds a layer of indirection that makes the code harder to follow. The concrete Stack class is all we need.
5. Lines 43-45. The __init__ docstring says 'with optional maximum capacity constraint' but that's just restating the signature. The actual implementation is solid, though the _operation_count field (line 45) is initialized and incremented but never used, making it more dead code.
6. Lines 59-61. The pop() method logs its operation at debug level, then line 61 increments _operation_count which nobody reads. If you need operation tracking, either use it for something or remove it. This ghost metric pattern shows up throughout.
7. Lines 71-81. Good instinct to add is_empty(), size(), and clear() helper methods, those are all legitimate. The type annotations with -> None, -> bool, -> int are appropriate and help readability.
8. Lines 83-89. The if __name__ == '__main__' block is a nice touch for demonstrating usage. However, there's no actual exception testing. If you're providing a max_size parameter and custom exception types, the demo should show what happens when the stack is full or when you pop from empty to validate the error handling works.